提交 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_性能测试项目管理与批量执行.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
# 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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论