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

docs(device-sim): 更新 HANDOFF 记录设备模拟模块部署到 5.60

- HANDOFF_设备模拟.md:新增会话 M(2026-08-21 部署 5 个后端文件到 5.60,
  含并行化/token 回退修复/无纸化 token+消息拉取),全链路验证通过
  (grep 校验 + 4 台批量启动 + 540 台全部停止),并同步会话 I/K 部署状态
- HANDOFF_性能测试.md:同步指标体系增强已部署 5.60 状态
- frontend/src/api/performance.ts:修复 listTasks/listProjects
  camelCase→snake_case 参数名转换(project_id/project_type 后端过滤生效)
- Docs/PRD/性能测试:新增问题处理与执行计划文档(项目内任务过滤、
  项目类型 Tab 过滤、批量执行跳转监控页 PRD)
- backend/scripts/restart_and_verify.py:新增 5.60 容器重启+健康检查脚本
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 26dbafbf
# 执行计划:修复性能测试项目内任务过滤不生效
> 生成时间:2026-08-21
> 关联问题文档:`_问题处理_项目内任务过滤不生效.md`
> 关联 PRD:`_PRD_性能测试项目管理与批量执行.md`
---
## 问题概述
性能测试模块的项目详情页任务列表不按项目过滤,显示全部任务。
**根因**:前端 API 传参使用驼峰 `projectId`,后端路由接收蛇形 `project_id`,Axios 默认不做转换,导致后端参数始终为 `None`,过滤逻辑被跳过。
## 修复方案
仅修改 `frontend/src/api/performance.ts``listTasks` 函数,在 API 层将 `projectId` 显式映射为 `project_id`
---
## 执行步骤
### Step 1: 修改前端 API 参数映射
**文件**`frontend/src/api/performance.ts`
**改动**:在 `listTasks` 函数中,构建请求参数时将 `projectId` 转换为 `project_id`
```typescript
export function listTasks(params?: {
page?: number
pageSize?: number
status?: string
projectId?: string | null
}): Promise<PerformanceTaskListResponse> {
const queryParams: Record<string, any> = { ...params }
// camelCase → snake_case: 后端路由参数为 project_id
if (params?.projectId !== undefined) {
queryParams.project_id = params.projectId
delete queryParams.projectId
}
return request.get(`${BASE}/tasks`, { params: queryParams })
}
```
**验收标准**
- `listTasks({ projectId: 'proj_xxx' })` → 发送 `?project_id=proj_xxx`
- `listTasks({ projectId: null })` → 发送 `?project_id=null`
- `listTasks({})` → 不发送 `project_id` 参数
### Step 2: 构建前端
```bash
cd frontend && npm run build
```
**验收标准**:构建成功,无 TypeScript 错误
### Step 3: 部署到 5.60 服务器
```bash
# SCP 前端 dist
scp -r frontend/dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
```
**验收标准**:文件上传成功
### Step 4: 验证
1. 浏览器 **Ctrl+F5** 强刷清除缓存
2. 进入性能测试 → 项目管理 → 点击「会议预约」项目
3. 任务列表应只显示该项目的任务
4. 浏览器 DevTools → Network → 查看请求 URL 参数是否为 `project_id=xxx`
---
## 改动清单
| 文件 | 类型 | 说明 |
|------|------|------|
| `frontend/src/api/performance.ts` | **MODIFY** | `listTasks` 函数增加 `projectId``project_id` 参数映射 |
**预计耗时**:< 10 分钟(改 1 处 + 构建 + 部署 + 验证)
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 执行计划:修复性能测试项目类型 Tab 过滤不生效
> 生成时间:2026-08-21
> 关联问题文档:`_问题处理_项目类型Tab过滤不生效.md`
> 关联修复:`_执行计划_修复项目内任务过滤不生效.md`
---
## 问题概述
性能测试项目管理页面的「标准版 / 项目定制」Tab 切换不生效,两个 Tab 显示完全相同的内容(全部项目)。
**根因**:前端 API 传参使用驼峰 `projectType`,后端路由接收蛇形 `project_type`,Axios 默认不做转换,导致后端参数始终为 `None`,过滤逻辑被跳过。
**与之前修复的 listTasks 是同一个根因**——前端 camelCase ↔ 后端 snake_case 命名不匹配。
## 修复方案
仅修改 `frontend/src/api/performance.ts``listProjects` 函数,在 API 层将 `projectType` 显式映射为 `project_type`
---
## 执行步骤
### Step 1: 修改前端 API 参数映射
**文件**`frontend/src/api/performance.ts`
**改动**:在 `listProjects` 函数中,构建请求参数时将 `projectType` 转换为 `project_type`
```typescript
/** 获取项目列表 */
export function listProjects(params?: {
page?: number
pageSize?: number
projectType?: string
}): Promise<PerformanceProjectListResponse> {
const queryParams: Record<string, any> = { ...params }
// camelCase → snake_case: 后端路由参数为 project_type
if (params?.projectType !== undefined) {
queryParams.project_type = params.projectType
delete queryParams.projectType
}
return request.get(`${BASE}/projects`, { params: queryParams })
}
```
**验收标准**
- `listProjects({ projectType: 'standard' })` → 发送 `?project_type=standard`
- `listProjects({ projectType: 'custom' })` → 发送 `?project_type=custom`
- `listProjects({})` → 不发送 `project_type` 参数(返回全部)
### Step 2: 构建前端
```bash
cd frontend && npm run build
```
**验收标准**:构建成功,无 TypeScript 错误
### Step 3: 部署到 5.60 服务器
```bash
# SCP 前端 dist
scp -r frontend/dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
```
**验收标准**:文件上传成功
### Step 4: 验证
1. 浏览器 **Ctrl+F5** 强刷清除缓存
2. 进入性能测试 → 项目管理
3. 默认「标准版」Tab → 确认只显示 standard 类型的项目
4. 点击「项目定制」Tab → 确认只显示 custom 类型的项目
5. 浏览器 DevTools → Network → 查看请求 URL 参数是否为 `project_type=xxx`
---
## 改动清单
| 文件 | 类型 | 说明 |
|------|------|------|
| `frontend/src/api/performance.ts` | **MODIFY** | `listProjects` 函数增加 `projectType``project_type` 参数映射 |
**预计耗时**:< 5 分钟(改 1 处 + 构建 + 部署 + 验证)
---
## 与之前修复的对比
| 修复项 | 文件 | 函数 | 映射 | 状态 |
|--------|------|------|------|------|
| 项目内任务过滤 | `performance.ts` | `listTasks` | `projectId``project_id` | ✅ 已部署 |
| 项目类型 Tab 过滤 | `performance.ts` | `listProjects` | `projectType``project_type` | 🔄 本次修复 |
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 执行计划:批量执行后自动跳转任务监控页
> 生成时间:2026-08-21
> 关联 PRD:`_PRD_批量执行跳转监控页.md`
> 关联文档:`HANDOFF_性能测试.md`、`_PRD_性能测试项目管理与批量执行.md`
---
## 问题概述
当前「执行全部」和「批量执行」按钮点击后,API 同步阻塞等待所有任务串行完成才返回,前端长时间无响应。用户无法实时查看任务执行情况,体验差。
**核心改动**
1. 批量执行 API 改为异步非阻塞(立即返回 `batch_id`
2. 新增批量执行详情 API 和批量监控页面
3. 前端按钮跳转到批量监控页,自动切换显示当前任务的实时指标
---
## 执行步骤
### Step 1: 后端 Schema — BatchRunResponse 新增 task_ids 字段
**文件**`backend/app/schemas/performance.py`
**改动**`BatchRunResponse` 类新增 `task_ids: List[str]` 字段
```python
class BatchRunResponse(BaseModel):
"""批量执行响应"""
message: str
batch_id: str
task_count: int
task_ids: List[str] = Field(default_factory=list, description="执行的任务ID列表")
report_id: Optional[str] = Field(None, description="合并报告ID(批量执行完成后生成)")
```
**验收标准**
- `BatchRunResponse` 包含 `task_ids` 字段
- 已有字段不受影响
---
### Step 2: 后端 Schema — 新增批量执行详情响应类型
**文件**`backend/app/schemas/performance.py`
**改动**:新增 `BatchTaskItem``BatchDetailResponse`
```python
class BatchTaskItem(BaseModel):
"""批量执行中的单个任务详情"""
id: str = Field(..., description="任务ID")
name: str = Field("", description="任务名称")
status: str = Field("pending", description="任务状态")
actual_tps: Optional[float] = None
avg_response_time: Optional[float] = None
error_rate: Optional[float] = None
actual_concurrency: Optional[int] = None
duration_actual: Optional[float] = None
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class BatchDetailResponse(BaseModel):
"""批量执行详情响应"""
batch_id: str
project_id: Optional[str] = None
status: str
total_count: int
success_count: int
fail_count: int
started_at: Optional[str] = None
tasks: List[BatchTaskItem] = Field(default_factory=list)
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
```
**验收标准**
- 新增类型定义正确,包含 camelCase 别名
- 与现有类型无冲突
---
### Step 3: 后端路由 — 批量执行改为异步非阻塞
**文件**`backend/app/routers/performance.py`
**改动**
1. `batch_run_tasks` 路由中,使用 `asyncio.create_task()` 在后台执行,立即返回 `batch_id``task_ids`
2. `run_project_all` 同上
**修改后代码**
```python
@router.post("/tasks/batch-run", response_model=BatchRunResponse)
async def batch_run_tasks(
data: BatchRunRequest,
service: PerformanceService = Depends(get_perf_service),
):
"""批量执行指定任务列表(非阻塞,后台执行)"""
if len(data.task_ids) == 0:
raise HTTPException(status_code=400, detail="任务列表不能为空")
# 立即创建批量执行记录并获取 batch_id
batch_id = service.create_batch_record(data.task_ids, data.project_id)
# 后台执行
asyncio.create_task(service.batch_run_tasks_async(
task_ids=data.task_ids,
project_id=data.project_id,
batch_id=batch_id,
))
return BatchRunResponse(
message="批量执行已启动",
batch_id=batch_id,
task_count=len(data.task_ids),
task_ids=data.task_ids,
report_id=None,
)
```
**验收标准**
- API 在 200ms 内返回(不等待任务执行完毕)
- 后台任务正确执行所有任务
- 批量执行完成后仍能生成合并报告
---
### Step 4: 后端 — 新增 `create_batch_record` 和 `batch_run_tasks_async` 方法
**文件**`backend/app/services/performance_service.py`
**改动**
1. 新增 `create_batch_record()` 方法 — 创建批量执行记录并返回 batch_id
2.`batch_run_tasks` 的方法体抽取为 `batch_run_tasks_async`(异步执行)
3. 新增 `get_batch_details()` 方法 — 查询批量执行详情
**关键代码示意**
```python
def create_batch_record(self, task_ids: List[str], project_id: Optional[str] = None) -> str:
"""创建批量执行记录,立即返回 batch_id"""
batch_id = generate_id("batch")
# 创建记录...
return batch_id
async def batch_run_tasks_async(self, task_ids, project_id, batch_id):
"""后台异步执行批量任务(独立事务,不依赖 API 请求上下文)"""
# 需要在独立事务中执行,创建独立的数据库会话
pass
async def get_batch_details(self, batch_id: str) -> Optional[Dict]:
"""获取批量执行详情(含各任务最新状态)"""
pass
```
**注意**:后台异步执行时,需要创建独立的数据库会话(`async_session_maker()`),因为 `run_in_executor` 线程池中的任务完成后回写到 `performance_tasks` 表,而 API 的 session 可能在后台任务执行期间被关闭。
---
### Step 5: 后端路由 — 新增批量执行详情 API
**文件**`backend/app/routers/performance.py`
**改动**:新增 `GET /batch-details/{batch_id}` 路由
```python
@router.get("/batch-details/{batch_id}", response_model=BatchDetailResponse)
async def get_batch_details(
batch_id: str,
service: PerformanceService = Depends(get_perf_service),
):
"""获取批量执行详情(含各任务状态)"""
details = await service.get_batch_details(batch_id)
if not details:
raise HTTPException(status_code=404, detail="批量执行记录不存在")
return details
```
**验收标准**
- `GET /api/performance/batch-details/batch_xxx` 返回正确的批量执行详情
- 包含各任务的当前状态、TPS、响应时间等
- 批量执行进行中返回实时状态
---
### Step 6: 前端类型定义 — 新增 BatchRunResponse 字段
**文件**`frontend/src/types/performance.ts`
**改动**`BatchRunResponse` 新增 `taskIds` 字段
```typescript
export interface BatchRunResponse {
message: string
batchId: string
taskCount: number
taskIds: string[] // 新增
reportId: string | null
}
```
**验收标准**:TypeScript 编译通过,无类型错误
---
### Step 7: 前端 API — 新增 getBatchDetails 函数
**文件**`frontend/src/api/performance.ts`
**改动**:在 `// ==================== 批量执行 ====================` 区域新增
```typescript
/** 获取批量执行详情 */
export function getBatchDetails(batchId: string): Promise<BatchDetailResponse> {
return request.get(`${BASE}/batch-details/${batchId}`)
}
```
**验收标准**:API 调用正常,返回数据格式正确
---
### Step 8: 前端类型定义 — 新增批量执行详情类型
**文件**`frontend/src/types/performance.ts`
**改动**:新增 `BatchTaskItem``BatchDetailResponse` 接口
```typescript
export interface BatchTaskItem {
id: string
name: string
status: string
actualTps: number | null
avgResponseTime: number | null
errorRate: number | null
actualConcurrency: number | null
durationActual: number | null
}
export interface BatchDetailResponse {
batchId: string
projectId: string | null
status: string
totalCount: number
successCount: number
failCount: number
startedAt: string | null
tasks: BatchTaskItem[]
}
```
**验收标准**:TypeScript 编译通过
---
### Step 9: 前端路由 — 新增 batch-monitor 路由
**文件**`frontend/src/router/index.ts`
**改动**:在 `performance` 子路由中新增
```typescript
{
path: 'batch-monitor',
name: 'PerfBatchMonitor',
component: () => import('@/views/performance/BatchMonitor.vue'),
meta: { title: '批量执行监控' }
}
```
**验收标准**:路由注册成功,访问 `/performance/batch-monitor?batchId=xxx` 进入页面
---
### Step 10: 前端 — 创建 BatchMonitor.vue 批量监控页面
**文件**`frontend/src/views/performance/BatchMonitor.vue`**NEW**
**组件结构**
```
BatchMonitor.vue
├── 顶部:返回按钮 + 项目名称 + 进度条
├── 任务列表表格(el-table,带颜色标记状态)
│ ├── 运行中 → 高亮行
│ ├── 已完成 → 显示 TPS/响应时间/错误率/操作(查看报告)
│ └── 等待中 → 灰色
├── 实时指标卡片(6 个,复用 MonitorPanel.vue 样式)
├── TPS 趋势图(ECharts)
├── 响应时间趋势图(含 P95)
├── 状态码分布图
├── 延迟与连接时间趋势图
└── 并发数趋势图
```
**核心逻辑**
```typescript
// 1. 挂载时从路由 query 获取 batchId 和 projectId
const batchId = computed(() => route.query.batchId as string)
const projectId = computed(() => route.query.projectId as string)
// 2. 加载批量执行详情
async function loadBatchDetail() {
const detail = await getBatchDetails(batchId.value)
tasks.value = detail.tasks
// 更新进度、成功数、失败数
}
// 3. 连接批量进度 WebSocket(ws/batch-progress)
// 接收 perf_batch_progress 消息,更新任务列表状态
// 4. 自动连接当前 running 任务的监控 WebSocket
// 监听 perf_snapshot 和 perf_complete 消息
// 当前任务完成 → 自动切换到下一个任务的 WebSocket
// 5. 定时轮询批量详情(每 2 秒,作为 WebSocket 的补充)
// 获取各任务的最新状态(completed 任务的 TPS 等指标)
```
**WebSocket 自动切换逻辑**
```
1. 收到 perf_batch_progress → current_index 更新
2. 如果 current_task_id 变化 → 关闭旧 WebSocket,连接新 task_id 的监控 WebSocket
3. 收到 perf_complete → 标记该任务完成,更新任务列表行
4. 收到 perf_batch_progress → current_status = completed/partially_failed → 全部完成
5. 全部完成 → 停止轮询,展示「查看合并报告」按钮
```
**验收标准**
- 页面加载后显示任务列表和进度条
- 当前运行中的任务实时显示 TPS 图表
- 任务完成后自动切换到下一个任务
- 全部完成后可查看合并报告
- 返回按钮回到项目详情页
---
### Step 11: 前端 — ProjectDetail.vue 修改执行全部跳转
**文件**`frontend/src/views/performance/ProjectDetail.vue`
**改动**`handleRunAll()``handleBatchRun()` 改为跳转批量监控页
**handleRunAll 修改后**
```typescript
async function handleRunAll() {
// ... 确认弹窗 ...
try {
const res = await runProjectAll(projectId.value)
// 立即跳转到批量监控页
router.push(`/performance/batch-monitor?batchId=${res.batchId}&projectId=${projectId.value}`)
} catch (e: any) {
ElMessage.error('执行失败: ' + (e.message || ''))
}
}
```
**handleBatchRun 修改后**
```typescript
async function handleBatchRun() {
// ... 确认弹窗 ...
try {
const res = await batchRunTasks({ taskIds: selectedIds.value, projectId: projectId.value })
// 立即跳转到批量监控页
router.push(`/performance/batch-monitor?batchId=${res.batchId}&projectId=${projectId.value}`)
} catch (e: any) {
ElMessage.error('批量执行失败: ' + (e.message || ''))
}
}
```
**不再需要**
- `batchProgressVisible` 相关代码(进度条移至 BatchMonitor 页面)
- `batchRunId``lastReportId` 相关逻辑
- `connectBatchWs()``showBatchProgress()` 方法
- 批量执行相关的 WebSocket 代码
**验收标准**
- 点击「执行全部」→ 确认弹窗 → API 调用 → 立即跳转到批量监控页
- 点击「批量执行(N)」→ 确认弹窗 → API 调用 → 立即跳转到批量监控页
- 旧版「查看合并报告」和「历史报告」按钮保留
---
### Step 12: 前端 — Projects.vue 修改执行全部跳转
**文件**`frontend/src/views/performance/Projects.vue`
**改动**`runProject()` 方法改为跳转批量监控页
```typescript
const runProject = async (project: PerformanceProject) => {
// ... 确认弹窗 ...
try {
const res = await runProjectAll(project.id)
router.push(`/performance/batch-monitor?batchId=${res.batchId}&projectId=${project.id}`)
} catch (error: any) {
ElMessage.error('执行失败: ' + (error.response?.data?.detail || error.message || ''))
}
}
```
**注意**`Projects.vue` 的「执行全部」按钮在项目卡片上,用户点击后直接跳转到批量监控页,不再停留在项目管理页等待。
**验收标准**:项目管理页点击「执行全部」→ 跳转到批量监控页
---
### Step 13: 构建前端
```bash
cd frontend && npm run build
```
**验收标准**:构建成功,无 TypeScript 错误
---
### Step 14: 部署到 5.60 服务器
```bash
scp -r frontend/dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
```
后端需重启:
```bash
ssh ubains@192.168.5.60 "sudo systemctl restart plat-auto-test"
```
**验收标准**:文件上传成功,后端重启成功
---
### Step 15: 验证
1. 浏览器 **Ctrl+F5** 强刷清除缓存
2. 进入 **性能测试 → 项目管理 → 点击项目**
3. 点击「执行全部」按钮
4. 确认:立即跳转到批量监控页,显示任务列表和进度条
5. 确认:当前任务显示实时 TPS 图表等指标
6. 确认:任务完成后自动切换到下一个任务
7. 确认:全部完成后显示「查看合并报告」按钮
8. 确认:DevTools → Network 中批量 API 在 200ms 内返回
---
## 改动清单
| 文件 | 类型 | 说明 |
|------|------|------|
| `backend/app/schemas/performance.py` | **MODIFY** | `BatchRunResponse` 新增 `task_ids`、新增 `BatchTaskItem``BatchDetailResponse` |
| `backend/app/routers/performance.py` | **MODIFY** | `batch_run_tasks`/`run_project_all` 改为异步非阻塞;新增 `GET /batch-details/{batch_id}` 路由 |
| `backend/app/services/performance_service.py` | **MODIFY** | 新增 `create_batch_record``batch_run_tasks_async``get_batch_details` 方法 |
| `frontend/src/types/performance.ts` | **MODIFY** | `BatchRunResponse` 新增 `taskIds`;新增 `BatchTaskItem``BatchDetailResponse` 接口 |
| `frontend/src/api/performance.ts` | **MODIFY** | 新增 `getBatchDetails` API 函数 |
| `frontend/src/views/performance/BatchMonitor.vue` | **NEW** | 批量监控页面(任务列表 + 实时指标 + ECharts 图表 + WebSocket 自动切换) |
| `frontend/src/views/performance/ProjectDetail.vue` | **MODIFY** | 「执行全部」和「批量执行」改为跳转批量监控页;移除原地进度条代码 |
| `frontend/src/views/performance/Projects.vue` | **MODIFY** | 「执行全部」改为跳转批量监控页 |
| `frontend/src/router/index.ts` | **MODIFY** | 新增 `batch-monitor` 路由 |
---
## 关键技术要点
### 后台异步执行注意事项
```python
# 在 FastAPI 中,后台任务需要独立数据库会话
# 因为 API 请求处理完成后的 session 会被关闭
async def batch_run_tasks_async(self, task_ids, project_id, batch_id):
"""后台异步批量执行"""
# 创建独立数据库会话
async with async_session_maker() as db:
service = PerformanceService(db)
# ... 执行逻辑 ...
await db.commit()
```
### WebSocket 自动切换逻辑
```typescript
let currentTaskWs: WebSocket | null = null
function connectTaskMonitor(taskId: string) {
// 关闭旧连接
if (currentTaskWs) {
currentTaskWs.close()
currentTaskWs = null
}
// 连接新任务的监控 WebSocket
currentTaskWs = createPerfMonitorStream(taskId, onSnapshot, onComplete, onError, onClose)
}
```
### 定时轮询作为 WebSocket 补充
WebSocket 推送 `perf_batch_progress` 消息时,仅包含 `current_index``success_count` 等计数,不包含每个任务的 TPS 等详细指标。需要在消息中新增 `tasks` 字段,或通过定时轮询 `GET /batch-details/{batch_id}` 获取各任务的最新状态:
```typescript
// 每 2 秒轮询批量详情
let pollTimer: number | null = null
function startPolling() {
pollTimer = window.setInterval(async () => {
const detail = await getBatchDetails(batchId.value)
tasks.value = detail.tasks // 更新任务列表表格
}, 2000)
}
```
**推荐方案**:WebSocket 推送 + 定时轮询结合:
- WebSocket 接收 `perf_batch_progress` 处理进度条和任务切换
- 定时轮询获取各任务详细指标更新表格
---
## 预计耗时
| 步骤 | 内容 | 预计时间 |
|------|------|---------|
| Step 1 | 后端 Schema 新增字段 | 10 min |
| Step 2 | 后端 Schema 新增类型 | 10 min |
| Step 3 | 后端路由改为异步 | 15 min |
| Step 4 | 后端 Service 新增方法 | 30 min |
| Step 5 | 后端路由新增详情 API | 10 min |
| Step 6-8 | 前端类型定义 + API | 15 min |
| Step 9 | 前端路由 | 5 min |
| Step 10 | BatchMonitor.vue 页面 | 90 min |
| Step 11 | ProjectDetail.vue 跳转 | 15 min |
| Step 12 | Projects.vue 跳转 | 10 min |
| Step 13 | 构建前端 | 5 min |
| Step 14 | 部署到 5.60 | 10 min |
| Step 15 | 验证 | 15 min |
| **合计** | | **~4 小时** |
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 问题处理:性能测试项目内任务过滤不生效
> 生成时间:2026-08-21
> 关联文档:`_PRD_性能测试项目管理与批量执行.md`、`HANDOFF_性能测试.md`
---
## 问题描述
在 5.60 服务器的测试管理平台上,点击进入「会议预约」文件夹(项目),页面上显示的性能测试任务列表中**混入了其他项目/未归属的任务**,即项目内任务过滤不生效。
## 复现步骤
1. 登录 5.60 测试管理平台
2. 进入「性能测试」→「项目管理」
3. 点击「会议预约」项目卡片进入详情页
4. 观察:任务列表显示的不是该项目的专属任务,而是包含其他项目的任务
## 当前行为
项目详情页任务列表显示**全部任务**(不区分项目),与预期只显示该项目下任务的行为不符。
## 预期行为
进入项目详情页后,任务列表**只显示该项目的任务**`project_id` 匹配),未归属任务和其他项目的任务不应出现。
---
## 根因分析
### 调用链回溯
```
ProjectDetail.vue:259
↓ 发送 params = { page: 1, pageSize: 20, projectId: "proj_xxx" }
performance.ts:42 → request.get('/api/performance/tasks', { params })
↓ Axios 默认序列化,参数名原样发送
GET /api/performance/tasks?page=1&pageSize=20&projectId=proj_xxx
performance.py:71 → project_id: Optional[str] = Query(None)
↓ 参数名不匹配,project_id = None
performance_service.py:113 → if project_id is not None:
↓ project_id 为 None,条件不成立,跳过过滤
返回全部任务(不区分项目)
```
### 根本原因
| 环节 | 文件 | 代码 | 参数名 |
|------|------|------|--------|
| 前端传参 | `ProjectDetail.vue:259` | `params.projectId = projectId.value` | `projectId`(驼峰) |
| 前端传参 | `TaskList.vue:484` | `params.projectId = projectFilter.value` | `projectId`(驼峰) |
| API 定义 | `performance.ts:35-43` | 参数类型声明为 `projectId?` | `projectId`(驼峰) |
| Axios 序列化 | `request.ts` | 无自定义 `paramsSerializer` | 参数名原样发送 |
| 后端路由 | `performance.py:71` | Query 参数名 `project_id` | `project_id`(蛇形) |
| 后端服务 | `performance_service.py:113` | `if project_id is not None:` | `project_id` 始终为 `None` |
**核心矛盾**:前端使用 JavaScript 驼峰命名 `projectId`,后端 FastAPI 使用 Python 蛇形命名 `project_id`。Axios 默认序列化参数名时不做转换,导致后端无法匹配到参数。
### 为什么其他功能正常?
- **分页参数**:前端 `page` / `pageSize` 与后端 `page` / `page_size` —— 注意后端路由 `page` 参数名也是 `page`(无下划线),所以匹配正常;`pageSize` 实际上后端 `page_size` 参数使用了 `Query(20, ...)` 的默认值,因为前端传的 `pageSize` 后端也不认识,但分页仍能工作是因为 `page_size` 有默认值 20
- **状态过滤**:前端 `status` 与后端 `status` 一致,所以正常工作
---
## 修复方案
### 方案一:前端 API 层映射(推荐,改动最小)
`frontend/src/api/performance.ts``listTasks` 函数中,将 `projectId` 显式映射为 `project_id`
```typescript
export function listTasks(params?: {
page?: number
pageSize?: number
status?: string
projectId?: string | null
}): Promise<PerformanceTaskListResponse> {
const queryParams: Record<string, any> = { ...params }
// 将 camelCase 的 projectId 映射为 snake_case 的 project_id
if (params?.projectId !== undefined) {
queryParams.project_id = params.projectId
delete queryParams.projectId
}
return request.get(`${BASE}/tasks`, { params: queryParams })
}
```
### 方案二:前端 Axios 全局参数序列化(全局修复,影响面大)
`request.ts` 中添加 `paramsSerializer`,将 camelCase 参数自动转为 snake_case。但可能影响其他模块的参数名,风险较高。
### 方案三:后端路由参数兼容驼峰(改动在后端)
在后端 `performance.py``list_tasks` 路由中,增加 `projectId` 的 Query 别名:
```python
project_id: Optional[str] = Query(None, alias="projectId", description="..."),
```
### 选定方案:方案一
改动最小、风险最低、不影响其他模块。仅影响 `listTasks` 一个函数。
---
## 影响范围
| 影响 | 说明 |
|------|------|
| `frontend/src/api/performance.ts` | `listTasks` 函数,需增加参数映射逻辑 |
| 后端 | 无需改动 |
| 其他前端文件 | 无需改动(`ProjectDetail.vue``TaskList.vue` 的传参方式不变) |
| 其他模块 | 无影响 |
## 验证方法
修复后执行以下验证:
1. **本地验证**:启动前端 + 后端,在浏览器中进入项目详情页 → 确认任务列表只显示该项目下的任务
2. **网络请求验证**:浏览器 DevTools → Network → 查看 `GET /api/performance/tasks` 请求 URL 应为 `?page=1&pageSize=20&project_id=proj_xxx`
3. **边界验证**`projectId=null`(查未归属)→ 应发送 `project_id=null``projectId` 不传 → 应不传 `project_id` 参数(返回全部)
---
## 部署步骤
1. 修改 `frontend/src/api/performance.ts`
2. `npm run build` 构建前端
3. SCP 前端 `dist/` 到 5.60 服务器
4. 浏览器 **Ctrl+F5 强刷** 清除缓存
5. 验证修复
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 问题处理:性能测试项目类型 Tab 过滤不生效
> 生成时间:2026-08-21
> 关联文档:`_PRD_性能测试项目管理与批量执行.md`、`HANDOFF_性能测试.md`
---
## 问题描述
在 5.60 服务器的测试管理平台上,性能测试项目管理页面的「标准版」和「项目定制」两个 Tab 页切换后,**显示的内容完全相同**,都只显示标准版的项目(或全部项目混在一起),没有按项目类型进行过滤。
## 复现步骤
1. 登录 5.60 测试管理平台
2. 进入「性能测试」→「项目管理」
3. 默认 Tab 为「标准版」,显示若干项目卡片
4. 点击「项目定制」Tab
5. 观察:页面仍然显示标准版的项目(或全部项目),与「标准版」Tab 下内容无区别
## 当前行为
两个 Tab 页显示相同内容,项目类型过滤不生效。
## 预期行为
- 切到「标准版」Tab:只显示 `project_type = 'standard'` 的项目
- 切到「项目定制」Tab:只显示 `project_type = 'custom'` 的项目
---
## 根因分析
### 调用链回溯
```
Projects.vue:172
↓ 发送 params = { page: 1, pageSize: 200, projectType: "standard" }
performance.ts:187 → request.get('/api/performance/projects', { params })
↓ Axios 默认序列化,参数名原样发送
GET /api/performance/projects?page=1&pageSize=200&projectType=standard
performance.py:408 → project_type: Optional[str] = Query(None)
↓ 参数名不匹配,project_type = None
performance_service.py:980 → if project_type:
↓ project_type 为 None,条件不成立,跳过过滤
返回全部项目(不区分类型)
```
### 根本原因
| 环节 | 文件 | 代码 | 参数名 |
|------|------|------|--------|
| 前端 Tab 切换 | `Projects.vue:140` | `projectType = ref('standard')` | `projectType`(驼峰) |
| 前端 API 调用 | `Projects.vue:172` | `listProjects({ projectType: projectType.value })` | `projectType`(驼峰) |
| API 定义 | `performance.ts:183-186` | 参数类型声明为 `projectType?` | `projectType`(驼峰) |
| Axios 序列化 | `request.ts` | 无自定义 `paramsSerializer` | 参数名原样发送 |
| 后端路由 | `performance.py:408` | Query 参数名 `project_type` | `project_type`(蛇形) |
| 后端服务 | `performance_service.py:980` | `if project_type:` | `project_type` 始终为 `None` |
**核心矛盾**:与之前修复的 `listTasks` 项目过滤问题**完全相同的根因**——前端使用 JavaScript 驼峰命名 `projectType`,后端 FastAPI 使用 Python 蛇形命名 `project_type`。Axios 默认序列化参数名时不做转换,导致后端无法匹配到参数。
### 为什么跟之前 listTasks 是同一个坑?
| 函数 | 前端参数 | 后端参数 | 当前状态 |
|------|---------|---------|---------|
| `listTasks` | `projectId` | `project_id` | ✅ 已修复(`Docs/PRD/性能测试/问题处理/_执行计划_修复项目内任务过滤不生效.md`) |
| `listProjects` | `projectType` | `project_type` | ❌ 未修复(本文档) |
---
## 修复方案
### 方案:前端 API 层映射(推荐,与 listTasks 修复模式一致)
`frontend/src/api/performance.ts``listProjects` 函数中,将 `projectType` 显式映射为 `project_type`
```typescript
export function listProjects(params?: {
page?: number
pageSize?: number
projectType?: string
}): Promise<PerformanceProjectListResponse> {
const queryParams: Record<string, any> = { ...params }
// 将 camelCase 的 projectType 映射为 snake_case 的 project_type
if (params?.projectType !== undefined) {
queryParams.project_type = params.projectType
delete queryParams.projectType
}
return request.get(`${BASE}/projects`, { params: queryParams })
}
```
---
## 影响范围
| 影响 | 说明 |
|------|------|
| `frontend/src/api/performance.ts` | `listProjects` 函数,需增加参数映射逻辑 |
| 后端 | 无需改动 |
| 其他前端文件 | 无需改动(`Projects.vue` 的传参方式不变) |
| 其他模块 | 无影响 |
## 验证方法
修复后执行以下验证:
1. **本地验证**:启动前端 + 后端,在浏览器中进入项目管理页 → 切换「标准版」Tab → 确认只显示 standard 项目;切换「项目定制」Tab → 确认只显示 custom 项目
2. **网络请求验证**:浏览器 DevTools → Network → 查看 `GET /api/performance/projects` 请求 URL 应为 `?page=1&pageSize=200&project_type=standard``project_type=custom`
3. **边界验证**`projectType` 不传 → 应不传 `project_type` 参数(返回全部)
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 需求文档:批量执行后自动跳转任务监控页
> 生成时间:2026-08-21
> 关联文档:`HANDOFF_性能测试.md`、`_PRD_性能测试项目管理与批量执行.md`
---
## 1. 背景与动机
当前性能测试模块的批量执行流程存在以下问题:
1. **API 阻塞**`POST /tasks/batch-run``POST /projects/{id}/run-all` 均为同步阻塞式 API,所有任务串行执行完毕后才返回响应,导致前端长时间无响应(可能数十秒甚至数分钟),用户体验极差。
2. **无监控跳转**:执行全部完成后,用户只能看到一个成功提示,无法直接跳转到监控页面查看每个任务的实时运行情况。
3. **批量进度仅靠进度条**`ProjectDetail.vue` 的批量执行进度条只展示简单的计数信息,无法看到单个任务的 TPS、响应时间等详细指标。
**用户诉求**:点击「执行全部」后,能立即跳转到监控页面,实时查看每个任务的性能测试运行情况(TPS、响应时间、状态码、并发数等图表)。
---
## 2. 现状分析
### 2.1 当前批量执行调用链
```
ProjectDetail.vue:handleRunAll()
runProjectAll(projectId)
POST /api/performance/projects/{projectId}/run-all
run_project_all() → batch_run_tasks() [同步阻塞,等待所有任务完成]
返回 BatchRunResponse { message, batch_id, task_count, report_id }
前端显示 ElMessage.success(res.message)
```
### 2.2 当前监控页(MonitorPanel.vue)
- 支持单任务实时监控(通过路由参数 `?taskId=xxx`
- WebSocket 连接 `/api/performance/ws/{taskId}` 接收 `perf_snapshot` 数据
- ECharts 展示 TPS 趋势、响应时间、状态码分布、延迟与并发
- 不支持批量任务监控
### 2.3 当前批量进度 WebSocket(`/ws/batch-progress`)
- 已有 `perf_batch_progress` 消息类型,包含:`batch_id``project_id``current_index``total_count``current_task_id``current_task_name``current_status``success_count``fail_count`
- 但前端 ProjectDetail.vue 仅用此进度更新简单的进度条数字
---
## 3. 功能需求
### FR-1: 批量执行 API 改为非阻塞
| 属性 | 说明 |
|------|------|
| 优先级 | P0 |
| 描述 | `batch-run``run-all` API 收到请求后立即返回 `batch_id``task_ids`,在后台异步执行任务,不再阻塞等待 |
**接口变更**
**Request**:无变化
**Response**`BatchRunResponse` 新增 `task_ids` 字段
```json
{
"message": "批量执行已启动",
"batch_id": "batch_xxx",
"task_count": 5,
"task_ids": ["task_001", "task_002", "task_003", "task_004", "task_005"],
"report_id": null
}
```
### FR-2: 新增批量执行详情 API
| 属性 | 说明 |
|------|------|
| 优先级 | P0 |
| 描述 | 新增 `GET /batch-details/{batch_id}` 接口,返回批量执行记录详情,含各任务的当前状态 |
**接口定义**
```
GET /api/performance/batch-details/{batch_id}
```
**Response**
```json
{
"batch_id": "batch_xxx",
"project_id": "proj_xxx",
"status": "running",
"total_count": 5,
"success_count": 2,
"fail_count": 0,
"started_at": "2026-08-21T08:00:00",
"tasks": [
{
"id": "task_001",
"name": "会议预约接口1",
"status": "completed",
"actual_tps": 120.5,
"avg_response_time": 85.3,
"error_rate": 0.02,
"actual_concurrency": 10,
"duration_actual": 60.0
},
{
"id": "task_002",
"name": "会议预约接口2",
"status": "running",
"actual_tps": null,
"avg_response_time": null,
"error_rate": null,
"actual_concurrency": null,
"duration_actual": null
},
{
"id": "task_003",
"name": "会议预约接口3",
"status": "pending",
...
}
]
}
```
### FR-3: 新增批量监控页面
| 属性 | 说明 |
|------|------|
| 优先级 | P0 |
| 描述 | 新建 `BatchMonitor.vue` 页面,批量执行启动后自动跳转,展示所有任务的实时状态和当前任务的性能指标 |
**路由**`/performance/batch-monitor?batchId=xxx&projectId=xxx`
**页面布局**
```
┌─────────────────────────────────────────────────────────┐
│ 批量执行监控 - 会议预约 │
│ 进度:███░░░░░░░ 3/10 成功 2 失败 0 [返回项目] │
├─────────────────────────────────────────────────────────┤
│ 任务列表 │
│ ┌──┬────────────┬────────┬───────┬──────┬────────┐ │
│ │# │ 任务名称 │ 状态 │ TPS │ 响应时间│ 操作 │ │
│ ├──┼────────────┼────────┼───────┼──────┼────────┤ │
│ │1 │ 会议预约-1 │ ✅完成 │120.5 │ 85ms │ 查看报告│ │
│ │2 │ 会议预约-2 │ 🔄运行中│ 98.2 │102ms │ 查看监控│ │
│ │3 │ 会议预约-3 │ ⏳等待中│ - │ - │ - │ │
│ │4 │ 会议预约-4 │ ⏳等待中│ - │ - │ - │ │
│ │5 │ 会议预约-5 │ ⏳等待中│ - │ - │ - │ │
│ └──┴────────────┴────────┴───────┴──────┴────────┘ │
├─────────────────────────────────────────────────────────┤
│ 当前任务实时指标(WebSocket) │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┐ │
│ │TPS │平均RT│请求数│错误率│峰值TPS│并发数│ │
│ │98.2 │102ms│ 2048 │ 0.1%│ 105.3│ 10 │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ TPS 趋势图(ECharts) │ │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ 响应时间趋势图(含 P95) │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
**核心功能**
1. **顶部进度条**:复用 ProjectDetail.vue 的进度样式,展示总进度 / 成功 / 失败
2. **任务列表表格**:展示所有任务的当前状态,完成后显示 TPS / 响应时间等关键指标
3. **实时指标卡片**:复用 MonitorPanel.vue 的 6 个指标卡片(TPS / 平均RT / 总请求数 / 错误率 / 峰值TPS / 并发数)
4. **实时图表**:复用 MonitorPanel.vue 的 ECharts 图表(TPS 趋势 / 响应时间趋势 / 状态码分布 / 延迟图 / 并发数图)
5. **自动切换**:当前任务完成后自动切换 WebSocket 到下一个任务,无需用户手动操作
6. **自动跳转完成**:批量执行全部完成后,自动展示「查看合并报告」按钮
### FR-4: 前端执行全部按钮跳转
| 属性 | 说明 |
|------|------|
| 优先级 | P0 |
| 描述 | `ProjectDetail.vue``Projects.vue` 的「执行全部」按钮点击后,调用 API 获取 `batch_id``task_ids` 后立即跳转到批量监控页 |
**流程**
```
用户点击「执行全部」
前端调用 POST /api/performance/projects/{projectId}/run-all
立即跳转 → /performance/batch-monitor?batchId={batch_id}&projectId={projectId}
BatchMonitor 页面:
1. 加载批量执行详情(GET /batch-details/{batch_id})
2. 连接批量进度 WebSocket(ws/batch-progress)获取全局进度
3. 自动连接第一个 running 任务的监控 WebSocket(ws/{task_id})
4. 任务完成时自动切换到下一个任务的 WebSocket
5. 全部完成后展示报告入口
```
### FR-5: 批量执行后台异步化
| 属性 | 说明 |
|------|------|
| 优先级 | P0 |
| 描述 | `batch_run_tasks()``run_project_all()` 改为 `asyncio.create_task()` 后台执行,API 立即返回 |
---
## 4. 非功能需求
| 类型 | 要求 |
|------|------|
| 性能 | 页面跳转延迟 < 500ms(API 响应 + 路由跳转) |
| 兼容性 | 支持 Chrome 90+ |
| 健壮性 | WebSocket 断开自动重连;后台任务异常不影响页面响应 |
| 回退兼容 | 旧版前端调用阻塞式 API 仍可正常等待结果(但新前端不再依赖此行为) |
---
## 5. 不在范围
- 批量执行中的任务取消/中断功能(后续迭代)
- 多个批量执行同时运行的管理(当前为串行,不支持并行批量)
- 移动端适配
---
## 6. 影响范围
| 文件 | 变更类型 | 说明 |
|------|---------|------|
| `backend/app/schemas/performance.py` | **MODIFY** | `BatchRunResponse` 新增 `task_ids` 字段 |
| `backend/app/routers/performance.py` | **MODIFY** | `batch_run_tasks``run_project_all` 改为后台异步;新增 `get_batch_details` 路由 |
| `backend/app/services/performance_service.py` | **MODIFY** | `batch_run_tasks` 改为后台执行模式 |
| `frontend/src/types/performance.ts` | **MODIFY** | `BatchRunResponse` 新增 `task_ids` 字段 |
| `frontend/src/api/performance.ts` | **MODIFY** | 新增 `getBatchDetails` API 函数 |
| `frontend/src/views/performance/BatchMonitor.vue` | **NEW** | 批量监控页面 |
| `frontend/src/views/performance/ProjectDetail.vue` | **MODIFY** | 「执行全部」和「批量执行」按钮跳转逻辑 |
| `frontend/src/views/performance/Projects.vue` | **MODIFY** | 「执行全部」按钮跳转逻辑 |
| `frontend/src/router/index.ts` | **MODIFY** | 新增 `batch-monitor` 路由 |
---
*本文档由 Claude Code 生成*
# HANDOFF_性能测试 # HANDOFF_性能测试
> 生成时间:2026-08-12 15:00 > 生成时间:2026-08-12 15:00
> 最后更新:2026-08-20 23:00 > 最后更新:2026-08-20 23:30
> 当前分支:`platform-auto-test` > 当前分支:`platform-auto-test`
> 最近提交:`a2c721e2 docs(performance): 更新 HANDOFF 记录项目管理与批量执行 + UI 预览文档` > 最近提交:`7cb9b7c1 docs(performance): 更新 HANDOFF 记录批量执行合并报告实现 + 540 设备批量启动验证`
> 主分支:`master` > 主分支:`master`
--- ---
...@@ -27,11 +27,40 @@ ...@@ -27,11 +27,40 @@
--- ---
## 🆕 最新会话进度(2026-08-20 18:00 ## 🆕 最新会话进度(2026-08-20 23:30 — 指标体系增强部署
### 本次会话完成的核心工作 ### 本次会话完成的工作
#### 0. 指标体系增强(10项新指标)✅(已完成,未部署) #### 指标体系增强(10项新指标)✅ 已部署 5.60
**部署时间**:2026-08-20 23:00–23:10
**部署文件**(后端 5 个):
- `backend/app/executors/performance_executor.py`
- `backend/app/services/performance_service.py`
- `backend/app/models/performance.py`
- `backend/app/schemas/performance.py`
- `backend/app/database.py`
**部署文件**(前端 dist 全量):
- `npm run build` 通过 ✅(40.35s,无 TS 错误)
- dist/ 全量上传至 `/data/third_party/plat-auto-test/frontend/dist/`
**部署步骤**
1. SCP 后端 5 文件 → `ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/app/`
2. SCP 前端 dist → `ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/`
3. `docker exec plat-auto-test-app sh -c "find /app -name '*.pyc' -delete"` 清理缓存
4. `docker compose restart app` → 等待 healthy
5. 验证 `_ensure_columns` 自动迁移:
- `performance_tasks` 新增 17 列(p95_response_time, std_dev, latency_avg, latency_min, latency_max, connect_time_avg, connect_time_max, peak_tps, total_sent_bytes, total_received_bytes, apdex, error_type_timeout, error_type_connect_error, error_type_client_error, error_type_http_4xx, error_type_http_5xx, error_type_assertion)
- `performance_snapshots` 新增 7 列(p95_response_time, std_dev, latency_avg, connect_time_avg, sent_bytes, received_bytes, peak_tps)
6. API 验证:`GET /api/performance/tasks` 返回新指标字段(p95ResponseTime: 0.0, stdDev: 0.0 等),旧任务均为 0(需重新执行才填充)
**部署踩坑**
- `find ... -delete` 在宿主机执行 Permission denied(ubains 无 `__pycache__` 写权限)→ 改用 `docker exec` 在容器内执行
- 历史任务新指标字段为 0 — 正常行为,只有部署后新执行的任务才会填充真实值
**遗留**:集成验证(跑一次短压测端到端验证新指标采集 → 快照 → 报告全流程)
**用户需求**:对标 JMeter 的市场标准,增强性能测试指标体系,提供更全面的分析维度。 **用户需求**:对标 JMeter 的市场标准,增强性能测试指标体系,提供更全面的分析维度。
...@@ -799,12 +828,12 @@ cd backend && uvicorn app.main:app --reload --port 8002 ...@@ -799,12 +828,12 @@ cd backend && uvicorn app.main:app --reload --port 8002
## 当前运行状态 ## 当前运行状态
### 生产服务器 192.168.5.60(2026-08-20) ### 生产服务器 192.168.5.60(2026-08-20 23:10
- 容器 `plat-auto-test-app`:healthy,内部端口 80(宿主机 80) - 容器 `plat-auto-test-app`:healthy,内部端口 80(宿主机 80)
- **当前部署版本**:含 curl 导入 + 自定义凭据 + 读取系统配置 URL + **项目管理与批量执行 + 合并报告**(2026-08-20 夜间全量部署) - **当前部署版本**:含 curl 导入 + 自定义凭据 + 读取系统配置 URL + 项目管理与批量执行 + 合并报告 + **指标体系增强(10项新指标)**(2026-08-20 夜间全量部署)
- **已部署**:后端 5 文件 + 前端 dist(Projects/ProjectDetail/ProjectReport + TaskList 适配 + 路由 - **已部署**:后端 5 文件 + 前端 dist(指标体系增强指标卡片 + 3 新图表 + 16 指标分位数表/错误分类表/延迟分解图/带宽趋势图
- **已部署**:合并报告 API 5 端点全部验证通过 - **已部署**:合并报告 API 5 端点全部验证通过
- **待部署**:指标体系增强(10 项新指标,本次未部署,不含在合并报告会话中 - **已部署**:指标体系增强 DB 迁移(`performance_tasks` 17 列 + `performance_snapshots` 7 列全部创建 ✅
- 任务 `perf_5a1a3b4e00e04b2f96713f4108580d2b` 验证通过(169 请求 / 0 失败 / TPS 10.96 / message_ids 捕获 2 个) - 任务 `perf_5a1a3b4e00e04b2f96713f4108580d2b` 验证通过(169 请求 / 0 失败 / TPS 10.96 / message_ids 捕获 2 个)
- MySQL 表 `performance_task_outputs` 已存在且有数据 - MySQL 表 `performance_task_outputs` 已存在且有数据
...@@ -812,7 +841,7 @@ cd backend && uvicorn app.main:app --reload --port 8002 ...@@ -812,7 +841,7 @@ cd backend && uvicorn app.main:app --reload --port 8002
- 后端:`python -m uvicorn app.main:app --port 8001`(当前无 --reload 运行中) - 后端:`python -m uvicorn app.main:app --port 8001`(当前无 --reload 运行中)
- 前端:`http://localhost:3000` - 前端:`http://localhost:3000`
- 本地数据库 `data/test_platform.db` 含历史任务 - 本地数据库 `data/test_platform.db` 含历史任务
- **最新改动**:指标体系增强(10 项新指标,25 个单元测试全部通过 ✅)+ **项目管理与批量执行**(前端 3 文件 + 路由 + 菜单,构建 30.52s 通过 ✅)+ **批量执行合并报告**(后端聚合 + 前端 ProjectReport + 5 条 API,npm run build 通过 ✅) - **最新改动**:指标体系增强(10 项新指标,25 个单元测试全部通过 ✅)+ **项目管理与批量执行**(前端 3 文件 + 路由 + 菜单,构建 30.52s 通过 ✅)+ **批量执行合并报告**(后端聚合 + 前端 ProjectReport + 5 条 API,npm run build 通过 ✅)+ **指标体系增强已部署 5.60** ✅
- **前端构建**:`npm run build` 通过 ✅ - **前端构建**:`npm run build` 通过 ✅
--- ---
...@@ -886,13 +915,9 @@ frontend/src/types/performance.ts (MODIFIED: +新指标类型 ...@@ -886,13 +915,9 @@ frontend/src/types/performance.ts (MODIFIED: +新指标类型
已部署 + 5 端点实测通过,但**前端页面流程**(进入项目详情 → 批量执行 → 查看合并报告 → 历史切换)建议在浏览器端完整走一遍回归。 已部署 + 5 端点实测通过,但**前端页面流程**(进入项目详情 → 批量执行 → 查看合并报告 → 历史切换)建议在浏览器端完整走一遍回归。
### 指标体系增强部署(待部署 5.60 ### 指标体系增强部署(已部署 5.60 ✅
指标体系增强(10 项新指标)尚未部署到 5.60 服务器。部署清单: 指标体系增强(10 项新指标)已于 2026-08-20 23:10 部署到 5.60 服务器。详见上方「2026-08-20 23:30 — 指标体系增强部署」节。
- 后端文件(5 个):`executors/performance_executor.py``services/performance_service.py``models/performance.py``schemas/performance.py``database.py`
- 前端 dist 全量(已构建通过)
- 部署步骤:scp 后端文件 → 清理 *.pyc → `docker restart plat-auto-test-app` → scp 前端 dist → 浏览器 Ctrl+F5 强刷
- 部署后验证:`GET /api/performance/tasks/{旧任务ID}/report` 返回新指标字段(p95/std_dev/apdex 等)
### 集成验证(待完成) ### 集成验证(待完成)
......
# HANDOFF — 设备模拟模块 # HANDOFF — 设备模拟模块
> **生成时间**: 2026-08-20 > **生成时间**: 2026-08-21
> **当前分支**: `platform-auto-test` > **当前分支**: `platform-auto-test`
> **最近提交**: `ae60263c` feat(performance): 前端合并报告页面 + 项目详情页查看合并报告入口 > **最近提交**: `7cb9b7c1` docs(performance): 更新 HANDOFF 记录批量执行合并报告实现 + 540 设备批量启动验证
> **未提交改动**: 无(设备模拟与性能测试改动均已提交推送 > **未提交改动**: 仅 `backend/data/test_platform.db`(本地 SQLite,已 gitignore
--- ---
## 📊 会话进度记录 ## 📊 会话进度记录
### 2026-08-21 会话 M:设备模拟未部署改动部署到 5.60(已部署并验证通过)
**会话目标**:将自上次部署 commit `695460eb` 以来累积的设备模拟改动全部部署到 5.60 生产环境并验证。
**状态**:✅ 部署完成 + 全链路验证通过(文件上传 / 容器重启 / 健康检查 / grep 内容校验 / 4 台批量启动 / 540 台全部停止)
**部署内容**(5 个后端文件,SFTP 上传 + `docker restart`):
| 文件 | 覆盖的改动 |
|------|-----------|
| `app/services/device_sim_service.py` | 会话 K 批量启动并行化(ThreadPoolExecutor 50 线程 + Semaphore)+ per-thread engine(`_thread_db`)+ 会话 J token_api_host 不回退 broker_host + 消息队列改 `thread_queue.Queue` |
| `app/services/mqtt_manager.py` | 会话 J on_disconnect 去阻塞(移除 sleep + reconnect,交给 paho 内置自动重连) |
| `app/simulators/door_token_client.py` | 会话 I 无纸化 token API 路径(无 /exapi 前缀)+ `get_message_by_app_token()` + `generate_paperless_reg_udid()` |
| `app/simulators/paperless_simulator.py` | 会话 I 启动四步流程(token 获取 + 消息定时拉取) |
| `app/database.py` | 性能测试增强指标列 + 预设登录凭据列 + 项目归属列(旧库升级兼容) |
**验证结果**
| 步骤 | 结果 |
|------|------|
| SFTP 上传(文件大小校验) | ✅ 5/5 |
| `docker restart plat-auto-test-app` + /health | ✅ healthy |
| grep 校验 8 项改动(并行化 / per-thread / token 回退 / thread_queue / on_disconnect / 无纸化路径 / 消息接口 / 消息拉取) | ✅ 全部命中 |
| 设备 stats API | ✅ `{"total":540,"byType":{"door":{"total":540}}}` |
| 4 台门口屏批量启动 | ✅ `successCount:4, failedCount:0`,启动后 `running:4` |
| 全部停止清理 | ✅ `成功 540, 失败 0` |
**新增脚本**`backend/scripts/`,可复用于后续部署):
| 脚本 | 用途 |
|------|------|
| `deploy_device_sim_560.py` | SFTP 上传 + 重启 + 健康检查 + API 验证(参数化,含 UPLOADS 清单) |
| `restart_and_verify.py` | 单独重启 + 健康轮询 + 容器状态 + stats API |
| `verify_deploy.py` | grep 校验服务器文件内容(8 项检查) |
| `test_batch_start.py` | 小批量启动冒烟测试(查询 → 批量启动 → 统计 → 全部停止) |
| `check_mounts.py` | 确认容器 bind mount(backend → /app) |
**本次踩坑**(补充踩坑记录):
| 现象 | 根因 | 正确做法 |
|------|------|---------|
| `docker compose restart``no configuration file provided: not found` | compose 文件在 `/data/third_party/plat-auto-test/deploy/` 子目录,从 HOME 执行找不到 | 直接 `docker restart plat-auto-test-app`(单容器不依赖 compose 文件) |
| `curl /api/device-sim/devices/?device_type=door` 返回 HTML | 尾斜杠命中前端 SPA 兜底路由返回 index.html | API 调用不带尾斜杠:`/api/device-sim/devices?...` |
**修改文件清单(本次会话)**:仅新增 5 个部署/验证脚本(见上表),业务代码零改动——本会话为纯部署会话。
**待办**
- 已知问题:MQTT Broker 闪断(rc=7)为 Broker 端问题,非 app 代码缺陷,持续观察
- 无纸化 token 获取 / 消息拉取已部署但需环境配置 `token_api_host`(如 `https://192.168.5.240`)才会实际触发
---
### 2026-08-20 会话 L:5.44 平台 8081 访问转圈问题排查与修复(已恢复)
**会话目标**:排查 5.44 测试管理平台 http://192.168.5.44:8081/ 访问一直转圈问题。
**状态**:✅ 服务已恢复,访问正常
---
#### 问题现象
`http://192.168.5.44:8081/` 浏览器一直转圈无响应,TCP 端口 8081 可连接但 HTTP 请求全部超时。
#### 排查过程
| 步骤 | 结果 |
|------|------|
| TCP 连接 192.168.5.44:8081 | ✅ 可连接(排除防火墙) |
| curl http://localhost:8081/health(5.44 本机) | ❌ `000`(应用层无响应) |
| `docker inspect` 容器状态 | `Up 7 hours (unhealthy)`,RestartCount=0(进程僵死,未崩溃) |
| `docker logs` 容器日志 | **MQTT 每 3 秒断连重连刷屏 7 小时**`env_12d8cff152a24e21a088d31bd0b2c791`,Broker `192.168.5.44:1883`,用户名 `mqtt@cmdb`) |
#### 根因
MQTT 连接反复断开→重连循环(rc=7 协议错误),持续 7 小时每 3 秒一轮刷屏日志,耗尽 FastAPI 事件循环线程池,导致应用僵死无法处理任何 HTTP 请求。TCP 端口由 Docker 端口映射保持监听,故外部 TCP 能通但 HTTP 无响应。
#### 修复
`docker restart plat-auto-test-app` → 等待 ~16 秒后 /health 返回 200,首页 200,MQTT 日志稳定无异常。重启后 MQTT 连接正常,属偶发 Broker 端不稳定。
#### 关键信息(环境探查附带收获)
| 项 | 值 |
|---|---|
| 5.44 MySQL 容器 | `plat-auto-test-mysql`,root 密码 `Ubains@13579`,库 `plat_auto_test` |
| MQTT 环境配置表 | **`device_env_configs`**(非 `env_configs`) |
| 断连环境 | `env_12d8cff152a24e21a088d31bd0b2c791`,Broker `192.168.5.44:1883`,MQTT 用户 `mqtt@cmdb`,topic_prefix=`device` |
| 5.44 上还有其他业务容器 | upython/upython_player/upython_voice/unginx/uemqx/uredis/umysql/unacos 等(host 网络模式) |
**修改文件清单(本次会话)**:无——操作均为运维层面(docker restart),无代码改动。
**待办**
- ⚠️ MQTT Broker 闪断(rc=7)仍为已知问题,为 Broker 端不稳定所致,非 app 代码缺陷,持续观察
- 可选增强:MQTT 断连重试增加退避上限或告警,避免再次耗尽事件循环
---
### 2026-08-19 会话 E:门口屏模拟器 Token 获取(已实现 + 真实调用验证通过,未提交未部署) ### 2026-08-19 会话 E:门口屏模拟器 Token 获取(已实现 + 真实调用验证通过,未提交未部署)
**会话目标**:为门口屏(door)模拟器补充启动时的真实 HTTP 接口调用链 —— 模拟真实 Android 门口屏设备(UbGview 应用)启动第一步调用的 `POST /exapi/api-android/token/getTokenInfoByToken` 接口获取 token。 **会话目标**:为门口屏(door)模拟器补充启动时的真实 HTTP 接口调用链 —— 模拟真实 Android 门口屏设备(UbGview 应用)启动第一步调用的 `POST /exapi/api-android/token/getTokenInfoByToken` 接口获取 token。
...@@ -1018,7 +1116,7 @@ def on_disconnect(self, client, userdata, rc): ...@@ -1018,7 +1116,7 @@ def on_disconnect(self, client, userdata, rc):
1. **启动时**调用 `POST /api/api-android/token/getTokenInfoByToken` 获取 token(与门口屏差异:无 `/exapi` 前缀 + regUdid 纯 16 位 hex 无前缀) 1. **启动时**调用 `POST /api/api-android/token/getTokenInfoByToken` 获取 token(与门口屏差异:无 `/exapi` 前缀 + regUdid 纯 16 位 hex 无前缀)
2. **启动后**按上报间隔定时调用 `POST /api/message/getMessageByAppToken` 拉取会议消息(HTTP 200 即视为成功,业务码 `A0034 会议不存在` 是正常业务场景) 2. **启动后**按上报间隔定时调用 `POST /api/message/getMessageByAppToken` 拉取会议消息(HTTP 200 即视为成功,业务码 `A0034 会议不存在` 是正常业务场景)
**状态**:✅ 文档(2 份 PRD + 2 份执行计划)+ 代码实现全部完成;✅ 真实接口调用验证通过(token 接口 HTTP 200 + 字段完整;消息接口 HTTP 200 成功);⚠️ 未提交 git、未部署 **状态**:✅ 文档(2 份 PRD + 2 份执行计划)+ 代码实现全部完成;✅ 真实接口调用验证通过(token 接口 HTTP 200 + 字段完整;消息接口 HTTP 200 成功);✅ 已提交 git(`695460eb`)并推送;✅ **已部署到 192.168.5.60 并验证通过**
**背景**:无纸化会议设备(华为平板 BAH3-W59,App 无纸化2.0)启动后的第一个动作是调用 token 接口,获取 Authorization 后定期调用消息接口。模拟器此前缺失此行为,启动时直接连 MQTT,且 `topic_params` 依赖手动配置。 **背景**:无纸化会议设备(华为平板 BAH3-W59,App 无纸化2.0)启动后的第一个动作是调用 token 接口,获取 Authorization 后定期调用消息接口。模拟器此前缺失此行为,启动时直接连 MQTT,且 `topic_params` 依赖手动配置。
...@@ -1119,7 +1217,7 @@ start() ...@@ -1119,7 +1217,7 @@ start()
**会话目标**:P1 优化 —— 将 `batch_start_simulators` 从串行执行改为并行执行,解决 540 台设备全部启动超时(>120s)问题。 **会话目标**:P1 优化 —— 将 `batch_start_simulators` 从串行执行改为并行执行,解决 540 台设备全部启动超时(>120s)问题。
**状态**:✅ 代码实现完成 + 语法检查通过 + 本地并发验证通过;✅ 已提交 git(`01c44f17`)并推送;⚠️ 未部署到 5.60 **状态**:✅ 代码实现完成 + 语法检查通过 + 本地并发验证通过;✅ 已提交 git(`01c44f17`)并推送;**已部署到 5.60 并验证通过(4 台批量启动 + 540 台全部停止)**
**问题背景**`batch_start_simulators()` 使用 for 循环逐台 `await start_simulator()` 串行启动。540 台设备串行启动: **问题背景**`batch_start_simulators()` 使用 for 循环逐台 `await start_simulator()` 串行启动。540 台设备串行启动:
- 正常 MQTT(0.5s/台):540 × 0.5s ≈ **270 秒** > 120s 超时 - 正常 MQTT(0.5s/台):540 × 0.5s ≈ **270 秒** > 120s 超时
...@@ -1284,7 +1382,5 @@ def _get_thread_engine(): ...@@ -1284,7 +1382,5 @@ def _get_thread_engine():
| `Docs/PRD/性能测试/需求文档/_PRD_批量执行合并报告_计划执行.md` | 新增 | 执行计划文档 | | `Docs/PRD/性能测试/需求文档/_PRD_批量执行合并报告_计划执行.md` | 新增 | 执行计划文档 |
**待办** **待办**
- ⚠️ 设备模拟相关改动(会话 J token_api_host 回退修复 + 会话 I 无纸化 token/消息 + 会话 K 并行化 + per-thread engine)**全部已提交并推送** - ✅ 设备模拟相关改动(会话 J token_api_host 回退修复 + 会话 I 无纸化 token/消息 + 会话 K 并行化 + per-thread engine)**全部已提交并推送**,且 **2026-08-21 已部署到 5.60**(SFTP 上传 5 个文件 + docker restart + 健康检查 + grep 验证 + 4 台批量启动验证通过)
- ⚠️ **未部署到 5.60**:需 SFTP 上传 `device_sim_service.py` + `mqtt_manager.py` + `door_token_client.py` + `paperless_simulator.py` + 其他 5 个模拟器文件
- ⚠️ 合并报告功能(Phase 1-2)已提交推送,待 Phase 3 前端构建验证 + 部署
- 已知问题:MQTT Broker 闪断(rc=7)为 Broker 端问题,非 app 代码缺陷 - 已知问题:MQTT Broker 闪断(rc=7)为 Broker 端问题,非 app 代码缺陷
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""重启 5.60 容器并验证健康状态"""
import paramiko, time
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('192.168.5.60', username='ubains', password='Ubains@123', timeout=30)
# 1. 重启容器
print(">>> docker restart plat-auto-test-app")
stdin, stdout, stderr = client.exec_command('docker restart plat-auto-test-app', timeout=180)
out = stdout.read().decode('utf-8', errors='replace').strip()
err = stderr.read().decode('utf-8', errors='replace').strip()[:200]
print(f" result: {out or err}")
time.sleep(15)
# 2. 健康检查
print("\n>>> 健康检查")
for i in range(10):
stdin2, stdout2, stderr2 = client.exec_command('curl -s http://localhost/health', timeout=30)
out2 = stdout2.read().decode('utf-8', errors='replace').strip()
if out2:
print(f" health({i+1}): {out2[:100]}")
if 'healthy' in out2 or 'ok' in out2.lower() or '200' in out2:
print(" ✅ 健康检查通过")
break
else:
print(f" ({i+1}) 无响应,等待5秒...")
time.sleep(5)
else:
print(" ❌ 健康检查未通过")
# 3. 容器状态
print("\n>>> 容器状态")
stdin3, stdout3, stderr3 = client.exec_command(
"docker ps --filter name=plat-auto-test-app --format '{{.Names}} {{.Status}}'",
timeout=30
)
print(f" {stdout3.read().decode('utf-8', errors='replace').strip()}")
# 4. API 验证
print("\n>>> API 验证")
stdin4, stdout4, stderr4 = client.exec_command(
'curl -s "http://localhost/api/device-sim/devices/stats?device_type=door"',
timeout=30
)
out4 = stdout4.read().decode('utf-8', errors='replace').strip()
if out4:
print(f" 设备统计: {out4[:120]}")
else:
print(" ⚠️ 设备统计API无响应")
client.close()
print("\n✅ 部署验证完成")
\ No newline at end of file
...@@ -39,7 +39,13 @@ export function listTasks(params?: { ...@@ -39,7 +39,13 @@ export function listTasks(params?: {
/** 按项目过滤;传 null 查未归属任务,不传返回全部 */ /** 按项目过滤;传 null 查未归属任务,不传返回全部 */
projectId?: string | null projectId?: string | null
}): Promise<PerformanceTaskListResponse> { }): Promise<PerformanceTaskListResponse> {
return request.get(`${BASE}/tasks`, { params }) const queryParams: Record<string, any> = { ...params }
// camelCase → snake_case: 后端路由 Query 参数名为 project_id
if (params?.projectId !== undefined) {
queryParams.project_id = params.projectId
delete queryParams.projectId
}
return request.get(`${BASE}/tasks`, { params: queryParams })
} }
/** 获取任务详情 */ /** 获取任务详情 */
...@@ -179,7 +185,13 @@ export function listProjects(params?: { ...@@ -179,7 +185,13 @@ export function listProjects(params?: {
pageSize?: number pageSize?: number
projectType?: string projectType?: string
}): Promise<PerformanceProjectListResponse> { }): Promise<PerformanceProjectListResponse> {
return request.get(`${BASE}/projects`, { params }) const queryParams: Record<string, any> = { ...params }
// camelCase → snake_case: 后端路由参数为 project_type
if (params?.projectType !== undefined) {
queryParams.project_type = params.projectType
delete queryParams.projectType
}
return request.get(`${BASE}/projects`, { params: queryParams })
} }
/** 获取项目详情 */ /** 获取项目详情 */
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论