# HANDOFF_性能测试

> 生成时间：2026-08-12 15:00
> 最后更新：2026-08-19 18:05
> 当前分支：`platform-auto-test`
> 最近提交：`69c215fe feat(device-sim): 无纸化授权码 + 环境配置弹窗简化 + 批量删除性能优化`
> 主分支：`master`

---

## 整体状态

| 阶段 | 状态 | 说明 |
|------|------|------|
| Phase 1 - 数据模型 + Schema | ✅ 完成 | ORM 模型 + Pydantic 校验 |
| Phase 2 - 压测执行引擎 | ✅ 完成 | 轻量级 asyncio + aiohttp 自研引擎 |
| Phase 3 - 后端路由 + Service | ✅ 完成 | 11 个 API 路由 + Service 层 |
| Phase 4 - 前端页面 | ✅ 完成 | 任务管理/监控/报告 三个面板 |
| Phase 5 - 集成测试 | ✅ 完成核心 | 真实 API（会议预约）端到端验证通过（2026-08-17） |
| Phase 6 - 响应捕获 + 跨任务引用 | ✅ 完成核心 | capture_rules + performance_task_outputs + REF 占位符（2026-08-17） |
| **读取系统配置被测系统URL** | ✅ 完成 | 运行时读取系统配置，不再依赖任务 target_url（2026-08-19） |
| **任务级自定义登录凭据** | ✅ 完成 | 新建/编辑任务可填自定义用户名/密码，验证码仍固定 csba（2026-08-19） |
| **从 curl 自动生成压测任务** | ✅ 完成 | 新建弹窗「从 curl 导入」→ 解析回填表单（2026-08-19） |

---

## 🆕 最新会话进度（2026-08-19 18:05）

### 本次会话完成的核心工作

#### 0. 从 curl 自动生成压测任务 ✅（已在 5.60 部署）

**用户需求**：新建性能测试任务时，粘贴接口调用 curl（含 `-X`/URL/`-H`/`-d`）+ 补充账号密码/压测模式/并发数/时长/预热 → 自动回填表单生成压测任务，减少手动填写。

**范围（用户已确认）**：仅解析 + 回填新建任务表单，**不含**「另存为接口预设」联动。Authorization 头剥离 → 置登录开关，Cookie 保留到 headers。

**产出文档**：
- `Docs/PRD/性能测试/需求文档/_PRD_从curl自动生成压测任务.md`
- `Docs/PRD/性能测试/需求文档/_PRD_从curl自动生成压测任务_计划执行.md`

**后端（3 文件 + 测试）**：

| 文件 | 变更 |
|------|------|
| `backend/app/utils/curl_parser.py` | **新建** — 纯函数 curl 解析器，仅 `shlex` 标准库，无外部依赖 |
| `backend/app/schemas/performance.py` | **追加** — `CurlParseRequest` / `CurlParseResponse` schema |
| `backend/app/routers/performance.py` | **追加** — `POST /api/performance/parse-curl` 路由（ValueError → 400） |
| `backend/tests/test_curl_parser.py` | **新建** — 11 个单元测试覆盖全部场景 |

**解析器能力矩阵**：
- 支持：`-X/--request`、`-H/--header`（多个 `; ` 合并）、`-d/--data/--data-raw/--data-binary/--data-json`（多个 `&` 拼接 + JSON 解析回退 raw）、URL 位置参数、`--key=value` 内联赋值
- 认证识别：`Authorization: Bearer` → `auth_required=true` 并**从 headers 剥离**；`X-RANDOM/X-TIMESTAMP/X-SIGN` → `sign_request=true` 并剥离；`Cookie` **保留**在 headers 并记录到 `cookie` 字段
- 方法推断：无 `-X` 时有 body → POST，否则 GET
- 不支持（进 `errors` 列表友好提示，不阻断）：`-F/--form` 文件上传、`-u/--user` Basic Auth、`--data-urlencode`
- 边界：空命令 / 无 URL → `ValueError` → 400

**⚠️ 关键决策**：`CurlParseRequest.curl_command` **故意不加 `min_length=1`** —— 否则 Pydantic 先返回 422，违反 PRD「空 curl → 400」约定；改为由 `parse_curl()` 抛 ValueError 转 400。

**前端（3 文件）**：

| 文件 | 变更 |
|------|------|
| `frontend/src/types/performance.ts` | **追加** — `CurlParseResult` 接口 |
| `frontend/src/api/performance.ts` | **追加** — `parseCurl(curlCommand)` API 封装 |
| `frontend/src/views/performance/TaskList.vue` | **修改** — 新建弹窗预设卡片旁「从 curl 导入」按钮 + 粘贴 dialog（textarea + errors 警告循环）+ 回填逻辑 |

**前端交互**：新建任务弹窗 → 「从 curl 导入」→ 粘贴 curl → 解析成功自动回填 method/targetUrl/headers/body/authRequired/signRequest，**清空 presetId 解除 disabled**，accountKey 回退默认；有 errors 弹警告提示；解析失败 ElMessage.error 提示。

**验证结果**：
- 后端单元测试：`python -m pytest tests/test_curl_parser.py` → **11 passed in 0.04s** ✅
- API 端点测试（httpx ASGITransport，8 项）：标准 POST / GET 推断 / Authorization 剥离 / 签名头剥离+Cookie 保留 / errors 列表 / 空命令 400 / 无 URL 400 / 内联赋值 → **全部通过** ✅
- 前端 `npm run build` → **built in 32.52s**，vue-tsc 类型检查无错误 ✅
- **已部署 5.60 服务器**（本次会话晚段）：前端 dist + 后端 7 文件（含 curl_parser.py），删 pyc + 重启容器 ✅

#### 0.5 第2批 + 第3批部署到 5.60 服务器 ✅（本次会话晚段完成）

**背景**：第2批（任务级自定义登录凭据）与第3批（curl 导入）本地改动均已完成验证但未部署；用户要求记录进度后部署到 5.60 服务器。

**执行步骤**：
1. scp 后端 6 文件：`app/utils/curl_parser.py`（新）、`app/models/performance.py`、`app/schemas/performance.py`、`app/services/performance_service.py`、`app/database.py`、`app/routers/performance.py` → `/data/third_party/plat-auto-test/backend/app/` 对应子目录 ✅
2. scp 前端 dist 全量 → `/data/third_party/plat-auto-test/frontend/dist/` ✅
3. `docker exec plat-auto-test-app sh -c "find /app -name '*.pyc' -delete"`（stale 字节码坑，必做）✅
4. `docker restart plat-auto-test-app` → 容器 `Up (healthy)` ✅

**服务器实测验证**：
- `GET /health` → `{"status":"healthy","service":"平台自动化测试系统","version":"1.0.0","debug":false}` ✅
- `POST /api/performance/parse-curl` 实测（带 `Authorization: Bearer` + `X-RANDOM` 签名头 + JSON body）：
  ```json
  {"method":"POST","targetUrl":"https://192.168.5.44/api/test","headers":{"Content-Type":"application/json"},
   "body":{"key":"val"},"authRequired":true,"signRequest":true,"cookie":null,"errors":[]}
  ```
  → Authorization 正确剥离置登录开关、签名头剥离置签名开关、body JSON 解析 ✅
- 空命令边界 → **HTTP 400** `{"detail":"curl 命令不能为空"}` ✅（符合 PRD 约定，非 422）
- `GET /api/performance/tasks` → 任务列表正常返回（MySQL 无异常，2 个历史任务）✅

**结论**：第2批 + 第3批改动已全部上线，用户可直接在新建任务弹窗使用「从 curl 导入」。

#### 1. 性能测试模块改为读取系统管理-系统配置的被测系统 URL ✅（本会话早段完成）

**背景**：系统管理-系统配置（`/system/settings`）已支持保存被测系统 URL，UI 自动化执行（`execution_service.py`）已运行时读取 `settings.TARGET_URL`。但性能测试模块存在两处问题：
1. `performance_service.py` 模块级常量 `_DEFAULT_BASE_URL = settings.TARGET_URL` 在 **import 时冻结**，运行时修改 `settings.TARGET_URL` 不生效
2. `_build_client_config()` 从任务的 `target_url` 字段解析 base_url，**覆盖**系统配置——切换被测环境要逐个改任务

**本次改动（按 PRD → 计划 → 代码流程，文档见 `Docs/PRD/性能测试/需求文档/`）**：

| 文件 | 变更 |
|------|------|
| `backend/app/config.py` | 新增 `AUTH_LOGIN_PATH`（默认 `/platform/api/auth/login`）、`AUTH_CAPTCHA_PATH`（默认 `/platform/api/code`） |
| `backend/app/models/dingtalk_config.py` | 新增 `login_path` / `captcha_path` 列（String(200)），`to_dict()` 返回 |
| `backend/app/schemas/dingtalk_config.py` | `SutConfigResponse`/`SutConfigUpdateRequest` 新增认证路径字段 |
| `backend/app/routers/system.py` | GET/PUT `/api/sut-config` 读写认证路径，PUT 校验路径以 `/` 开头 |
| `backend/app/database.py` | 旧库迁移：`dingtalk_configs` 加 `login_path`/`captcha_path` 列 |
| `backend/app/main.py` | **lifespan 启动时从数据库加载 `AUTH_LOGIN_PATH`/`AUTH_CAPTCHA_PATH`**（与 TARGET_URL 同段代码） |
| `backend/app/services/performance_service.py` | **删除 `_DEFAULT_BASE_URL` 模块常量与整个 `_DEFAULT_AUTH_CONFIG` dict**；`_build_client_config()` 改为运行时读取 `settings.TARGET_URL` / `AUTH_LOGIN_PATH` / `AUTH_CAPTCHA_PATH`，不再解析任务 `target_url` |
| `frontend/src/api/system.ts` | `updateSutConfig()` 改为接收 `{ url, login_path?, captcha_path? }` 对象 |
| `frontend/src/views/system/Settings.vue` | 被测系统配置新增「登录接口路径」「验证码接口路径」输入框 |
| `frontend/src/router/index.ts` | **注释掉未开发完成的 document 路由**（引用不存在的文件导致构建失败） |

**新版 `_build_client_config()` 行为**：
```python
@staticmethod
def _build_client_config(task_dict):
    return {
        "target": {
            "base_url": settings.TARGET_URL or "",   # 运行时读取，切换环境无需改任务
            "verify_ssl": False,
            "timeout": 30,
        },
        "accounts": dict(_DEFAULT_ACCOUNTS),
        "auth": {
            "login_path": settings.AUTH_LOGIN_PATH or "/platform/api/auth/login",
            "captcha_path": settings.AUTH_CAPTCHA_PATH or "/platform/api/code",
        },
    }
```

**验证结果**：
- 前端 `npm run build` ✅（修复 document 路由后 38.36s 构建通过，无 TS 错误）
- 服务器容器内验证 `_build_client_config({'target_url': 'http://old.example'})` → `base_url = https://192.168.5.44`（读取系统配置，任务字段不再生效）✅
- 服务器 `GET /api/system/sut-config` 返回 `{"url":"https://192.168.5.44","source":"db","login_path":"/platform/api/auth/login","captcha_path":"/platform/api/code"}` ✅
- 容器 healthy，`/health` 正常 ✅

**⚠️ 已部署到 5.60 服务器**（2026-08-19）：前端 dist + 7 个后端文件已 SCP，容器已重启。

#### 2. 任务级自定义登录用户名/密码 ✅（本次新增）

**用户需求**：系统登录的用户密码也需可配置，**在新建性能测试任务时提供输入**；验证码固定 `csba`。

**实现方式**：`PerformanceTask` 新增两个可选字段 `login_username` / `login_password`。新建/编辑任务时填写了自定义凭据 → 压测执行时使用该凭据登录；留空 → 回退 `_DEFAULT_ACCOUNTS` 默认值（admin@xty / Ubains@13579）。

**本次改动（按计划文件 `radiant-launching-eagle.md` 实施）**：

| 文件 | 变更 |
|------|------|
| `backend/app/models/performance.py` | `PerformanceTask` 新增 `login_username` / `login_password`（String(200), nullable=True），`to_dict()` 返回两字段 |
| `backend/app/schemas/performance.py` | `PerformanceTaskCreate` / `PerformanceTaskUpdate` / `PerformanceTaskResponse` 各新增两个 `Optional[str]` 字段 |
| `backend/app/database.py` | `_ensure_columns()` 追加 `performance_tasks.login_username` / `login_password` 两条迁移（VARCHAR(200) DEFAULT NULL，旧库自动补列） |
| `backend/app/services/performance_service.py` | `_build_client_config()` 中：若 task_dict 含 `login_username`+`login_password`，则以 `account_key` 为键覆盖 accounts 字典凭据，否则保持默认 |
| `frontend/src/types/performance.ts` | `PerformanceTask` / `Create` / `Update` 三个接口新增 `loginUsername?` / `loginPassword?` |
| `frontend/src/views/performance/TaskList.vue` | `TaskForm` + `defaultForm()` 新增两字段；弹窗「账号」选择器下方新增用户名/密码两个输入框（`form.authRequired` 时显示，仅 `el-input` 样式）；`handleSave()` 提交空串转 null；`openEditDialog()` 回填 |

**关键代码（`_build_client_config()` 凭据注入）**：
```python
# 构建 accounts 字典（优先使用自定义凭据）
accounts = dict(_DEFAULT_ACCOUNTS)
if task_dict.get("login_username") and task_dict.get("login_password"):
    key = task_dict.get("account_key", "custom")
    accounts[key] = {
        "username": task_dict["login_username"],
        "password": task_dict["login_password"],
    }
```

**无需改动**：`http_client.py`、`performance_executor.py`（`_get_login_body()` 已从 `http_client_config["accounts"]` 读取凭据，Service 注入后自动生效）、验证码固定 `csba`、`account_key` 选择器保留（兼容旧任务）。

**验证结果**：
- 前端 `npm run build` ✅（vue-tsc 类型检查 + vite 构建，2344 modules，30.64s）
- 后端 `py_compile` ✅（models / schemas / service / database 四个文件）
- **已部署 5.60 服务器**（本次会话与 curl 导入一起部署，2026-08-19）

**⚠️ 使用注意**：登录凭据（密码）以明文存库（`performance_tasks.login_password`），内网平台可接受；后续如做权限系统需考虑脱敏。

#### 3. 从 curl 自动生成压测任务 ✅（已实现 + 已部署 5.60）

详见本会话 「#### 0. 从 curl 自动生成压测任务」节，本小节原为待办，现已完成。

#### 4. 修复 curl 导入 `[object Object]`（P0 已修复 + 已部署 5.60）

**问题**：用户粘贴 Windows CMD 风格 curl 命令（含 `^` 续行符 + `^"` 转义引号）后，弹窗显示 `curl 解析失败: [object Object]`。

**根因（3 层叠加）**：
1. **主因**：`CurlParseRequest` 缺少 `model_config`，前端发送 camelCase `{curlCommand}` → Pydantic 无法映射到 `curl_command` → 422
2. **Windows CMD `^` 语法未处理**：`shlex.split()` 不认识 `^` 续行符和 `^"` 转义引号，导致 `^` 拼进 token（如 `^Authorization`）→ 认证/签名头检测失败
3. **缺失 `-b`/`--cookie` 支持**：`-b "sid=abc"` 被静默忽略

**修复（3 个文件）**：

| 文件 | 变更 |
|------|------|
| `backend/app/schemas/performance.py` | `CurlParseRequest` 补全 `model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)` |
| `backend/app/utils/curl_parser.py` | 新增 `_preprocess_cmd_style()` 还原 `^` 续行/`^"` 转义；新增 `-b`/`--cookie` 分支 |
| `backend/tests/test_curl_parser.py` | 新增 4 个测试用例（15 passed） |

**文档**：`Docs/PRD/性能测试/问题处理/_问题处理_curl导入解析失败[object Object].md` + `_执行计划_修复curl导入解析失败[object Object].md`

**部署**：scp → 清理 pyc → 重启容器 ✅
**验证**：camelCase `{curlCommand}` → 200 ✅；Windows CMD 完整 curl（Authorization + 签名头 + -b Cookie）→ authRequired=true / signRequest=true / cookie 有值 ✅

#### 5. 修复保存任务 422（P0 已修复 + 已部署 5.60）← 本次会话新增

**问题**：curl 导入解析成功后，点击保存按钮提示 `保存失败: [object Object],[object Object],[object Object],[object Object]`。用户质疑"确定更新了吗？怎么还是新增报错？"

**根因**：
1. **后端 schema 非 Optional 字段接收 null**：`PerformanceTaskCreate` 的 `qps_target / ramp_up / step_concurrency / step_duration` 定义为 `int`/`List[int]`（非 Optional）。前端在 `concurrent` 模式下给这些字段发 `null` → Pydantic v2 对非 Optional 字段拒绝 null → 422。
2. **前端错误信息不可读**：`e.response?.data?.detail` 是 Pydantic 错误对象数组，`.toString()` 变成 `[object Object],[object Object],[object Object],[object Object]`（4 个字段各一个校验错误）。
3. **`stepConcurrency` 数组双重包裹**：`[form.stepConcurrency]` 如果 `form.stepConcurrency` 本身已是数组（编辑回填时），会变成 `[[10, 20, 30]]` → 校验失败。

**修复（3 个文件）**：

| 文件 | 变更 |
|------|------|
| `backend/app/schemas/performance.py` | `qps_target/ramp_up/step_concurrency/step_duration` 改为 `Optional` + `default=None` |
| `backend/app/services/performance_service.py` | `create_task()` 中补齐 Optional 压测参数默认值（`100/10/[]/60`） |
| `frontend/src/views/performance/TaskList.vue` | 新增 `extractApiError()` 渲染 Pydantic 错误数组为可读文本；`handleSave()` 修正 `stepConcurrency` 双重包裹判断；`handleCurlParse` 错误处理改用 `extractApiError()` |

**部署（本次会话）**：
- 后端：scp `schemas/performance.py` + `services/performance_service.py` → 清理 pyc → `docker restart plat-auto-test-app` ✅
- 前端：`npm run build`（31.34s，无错误）→ scp `dist/*` 全量 ✅
- 验证：发送 `qpsTarget/rampUp/stepConcurrency/stepDuration` 全为 null → **201 创建成功**，DB 落库值 `100/10/[]/60` ✅

---

### 上一会话（2026-08-17）完成的核心工作

#### 1. 「新建会议压测」任务端到端验证通过 ✅

**任务**：`perf_5a1a3b4e00e04b2f96713f4108580d2b`（新建会议压测）
**目标接口**：`PUT https://192.168.5.44/meetingV3/api/message/book`（JWT + X-RANDOM/X-TIMESTAMP/X-SIGN 签名）
**最终配置**：concurrency=2（用户要求 2 线程快速验证）/ duration=15s / rampUp=0 / 时段 +37H~+38H / duration字段=60

**最终验证结果（干净代码，调试日志已清除）**：

| 指标 | 值 |
|------|-----|
| 总请求数 | 169 |
| 成功数 | 169（HTTP 200） |
| 失败数 | 0 |
| 错误率 | 0.0% |
| 实际 TPS | 10.96 |
| 平均响应时间 | 178.41ms（min 145 / max 231） |
| **message_ids 捕获** | ✅ `['2026-0817013311822', '2026-0817013311824']` |

捕获输出 API 验证：`GET /api/performance/tasks/{id}/outputs` → `{"total":1,"items":[{"key":"message_ids","value":[...]}]}`

#### 2. A0027「时间格式不正确」根因定位与修复 ✅（用户反馈的问题）

**现象**：服务日志持续打印 `body={"success":false,"code":"A0027","message":"时间格式不正确"}`，但 HTTP 状态 200 → 统计全"成功"，捕获为空。

**根因（双层）**：
1. **服务器上的 `template_resolver.py` 是旧版本**：`_resolve_token()` 对所有 token 用 `token.split(":")` 分割，而任务占位符是 `NOW_+30_M`（下划线分隔）→ `kind="NOW_+30_M"` 不匹配 `"NOW"` → 占位符**原样发送**（字面量 `{__NOW_+30_M__}`）→ API 返回 A0027。
   - 正确版本（本地）：REF 用 `:` 分割，其余（NOW/RANDOM_NAME/RANDOM_INT/UUID/INDEX）用 `_` 分割。
2. **stale `.pyc` 字节码缓存**：修复源码后容器仍加载旧字节码（`__pycache__/template_resolver.cpython-310.pyc` mtime 早于源码修改时间），必须删除 pyc + 重启容器。

**修复步骤（5.60 服务器）**：
```bash
# 1. 通过 paramiko base64 部署正确的 template_resolver.py（本地版本）
# 2. 删除字节码缓存
docker exec plat-auto-test-app sh -c "find /app -name '*.pyc' -delete"
# 3. 重启容器
docker restart plat-auto-test-app
# 4. 验证（容器内）
python3 /tmp/test_tr.py   # 输出 NOW: 2026-08-17 12:59:09（带秒）✓
```

**修复后 DBG_RESOLVE 日志确认**：`startTime: '2026-08-17 13:30:02'`（秒精度，占位符已替换）

#### 3. 会议预约 API 业务限制探测（重要经验）

探测脚本（本地 backend/meeting_test25.py 等）验证会议室 `29dcd33aa47797e7d29d79ae6711d597` 的规则：

| 错误码 | 含义 | 触发条件 | 规避方法 |
|--------|------|---------|---------|
| A0027 | 时间格式不正确 | startTime/endTime 非秒精度或占位符未解析 | 修复 resolver（已完成） |
| A0023 | 该时间段已有会议 | 目标时段与已有会议重叠 | 选无人占用时段（如 +37H 凌晨）；**注意探测脚本自己创建的会议也会占用时段** |
| A0026 | 超过会议室可预订的最大时长 | body 中 `duration=120`（该会议室上限 90 分钟） | `duration` ≤ 90 且与 start/end 跨度一致 |
| B0001 | failure | `cnum=""`（不指定会议室） | 必须携带有效会议室 ID |

**预约接口 body 必需字段**：JSON **数组** `[{...}]`、`convergentMediaTypes:"1"`、`companyNumber:"CN-WQF-UBAINS"`（JWT 声明一致）、`uid:8`（数字）、`configValueId:2`（数字）、有效 `cnum`、`participantList:[]`、秒精度时间。

#### 4. 压测任务设计注意事项（同一时段冲突问题）

**发现**：concurrent 模式下所有并发请求解析出**相同的 startTime/endTime**（`{__NOW_+37_H__}` 对所有请求解析结果相同）→ 第一批请求订走时段后，后续请求全部 A0023。

**当前状态**：2 并发时前 2 个请求成功（message_ids 捕获 2 个），其余 A0023（HTTP 200，统计不报错）。

**待优化方案**：给模板解析器增加按请求序号偏移时间的语法，例如 `{__NOW_+37_H_INDEX__}`（每请求 +1 分钟错开时段），需要在 `template_resolver.py` 的 `_resolve_token` 中处理 `parts[3] == "INDEX"` 分支（尚未实现）。

### 响应捕获（capture_rules）链路（本次已全链路验证）

```
任务 capture_rules（JSON）
  → executor._capture_response(rules, body_text, status)   # JSONPath $.result[*].messageId 提取
  → self._captured.setdefault(key, []).append(value)         # _capture_max=10000 上限
  → result["captured"]（execute() finally 块）
  → service._save_outputs(task_id, captured)                 # upsert 到 performance_task_outputs 表
  → GET /api/performance/tasks/{id}/outputs                  # 前端/下游任务读取
```

- 捕获仅在 HTTP 2xx 时执行；`_save_outputs` 空捕获时 early return（不写库）
- `PerformanceTaskOutput` 表（MySQL `performance_task_outputs`）按 task_id+key upsert
- 新增文件：`models/performance_output.py` / `routers/performance_output.py` / `schemas/performance_output.py`
- service 侧 `_save_outputs` 在 `run_task` 的 finally 中调用（此前服务器旧版 service 缺此函数，已部署本地新版修复）

### 服务器（192.168.5.60）部署情况

- 容器：`plat-auto-test-app`（volume mount `/data/third_party/plat-auto-test/backend:/app`，内部端口 80）
- **2026-08-17 部署**（paramiko + base64 方式，scp 不可用）：
  - `app/executors/template_resolver.py` — 正确的 `_` 分割版本（修复 A0027）
  - `app/services/performance_service.py` — 含 `_save_outputs` 的新版
  - `app/executors/performance_executor.py` — 已含捕获逻辑，临时加过 DBG_RESOLVE/DBG_CAPTURE 调试日志，**现已清除**（验证 grep DBG = 0）
- **2026-08-19 部署（第1批）**（scp 方式，已恢复正常）：
  - `app/config.py` — 新增 `AUTH_LOGIN_PATH` / `AUTH_CAPTCHA_PATH`
  - `app/database.py` — 旧库迁移 ADD COLUMN login_path / captcha_path
  - `app/main.py` — lifespan 加载认证路径
  - `app/models/dingtalk_config.py` — 新增 login_path / captcha_path 字段
  - `app/schemas/dingtalk_config.py` — SutConfig 新增字段
  - `app/routers/system.py` — GET/PUT 读写认证路径
  - `app/services/performance_service.py` — 删除 `_DEFAULT_BASE_URL`，运行时读取系统配置
  - 前端 `dist/` — 重构 Settings.vue（新增认证路径输入框）
- **2026-08-19 本地改动（第2批+第3批，已部署服务器）**：
  - 第2批：性能测试任务级自定义登录凭据（model + schema + service + 前端 TaskList 表单）
  - 第3批：从 curl 自动生成压测任务（curl_parser.py + 路由 + schema + 测试 + 前端导入交互）
  - 部署详情：scp 后端 6 个文件 + 前端 dist 全量 → 清理 *.pyc → 重启容器 ✅
- **部署后处理**：删除全部 `*.pyc` + 重启容器（否则旧字节码仍生效）
- 调试日志补丁模式（本次使用后已移除）：
  - `[DBG_RESOLVE]` 在 `_resolve_body` 的 resolve() 调用后打印解析结果
  - `[DBG_CAPTURE]` 在 `_capture_response` 开头打印规则/状态/响应体前 200 字符

### 本地会话遗留状态

- 本地 backend 以 `python -m uvicorn app.main:app --port 8001`（无 --reload）方式运行，日志 `/tmp/backend_8001.log`
- 本地验证脚本：`backend/meeting_test22.py` ~ `meeting_test25.py`（探测 API 最小必需字段/时段/时长限制，可复用）
- 本地任务 `perf_f27debe671d84b4fb3908af5beac67ed`（同名任务）尚未做完整验证（服务器版已验证通过）

**后端代码行数**：2173 行（executor 748 + service 588 + router 296 + model 279 + schema 262）
**前端代码行数**：1768 行（index 98 + TaskList 476 + MonitorPanel 459 + ReportPanel 373 + api 121 + types 241）

---

## 多窗口并行开发注意

如需同时开多个 Claude Code 窗口开发不同子菜单功能，请先阅读 `Docs/多窗口并行开发指南.md`。

性能测试模块的端口与文件：
- 后端：8001（默认）/ 8002（当前可用）
- 前端：3000
- 数据库：`data/test_platform.db`（共享同一个 SQLite）
- Git 分支：`platform-auto-test`

---

## 已完成事项

### 后端（Phase 1-3）

#### 1. 数据模型 (`backend/app/models/performance.py`)

| 模型 | 表名 | 说明 |
|------|------|------|
| `PerformanceTask` | `performance_tasks` | 任务主表（名称/URL/方法/模式/并发/时长/状态/认证/断言） |
| `PerformanceSnapshot` | `performance_snapshots` | 每秒快照（TPS/响应时间P50/P90/P99/状态码分布） |
| `PerformanceTaskResult` | `performance_task_results` | 最终结果（汇总指标 + JSON 详情） |

**关键字段**：
- `PerformanceTask.mode` → `concurrent` / `qps` / `step`
- `PerformanceTask.status` → `pending` / `running` / `completed` / `failed` / `cancelled`
- `PerformanceTask.account_key` → `superadmin` / `admin` / `user`
- `PerformanceSnapshot` 使用 camelCase 别名（`p50_response_time` → `p50` / `p90` / `p99`）

#### 2. Pydantic Schema (`backend/app/schemas/performance.py`)

- 全部 schema 使用 `populate_by_name=True, alias_generator=to_camel` 实现 camelCase 兼容
- 断言规则：`AssertionRule`（status_code / response_time / response_body + JSON Path）
- 请求/响应结构：`PerformanceTaskCreate` / `PerformanceTaskUpdate` / `PerformanceTaskResponse` / `PerformanceTaskListItem` / `PerformanceTaskListResponse`
- 执行相关：`PerformanceRunRequest` / `PerformanceRunResponse`
- 数据：`PerformanceSnapshotResponse` / `PerformanceSnapshotListResponse`
- 报告：`PerformanceReportResponse`（含 `summary` / `metrics` / `snapshots`）

#### 3. 压测执行引擎 (`backend/app/executors/performance_executor.py`)

**核心架构**：
- `MetricsCollector` — 线程安全指标采集器（带锁的 period 累积 + 全局累加）
  - `record()` 记录单次请求结果（响应时间 + 状态码 + 错误）
  - `snapshot()` 生成每秒快照（TPS / P50/P90/P99 / 状态码分布）
  - `summary()` 汇总最终指标
  - `snapshot_callback` 实时推送（WebSocket 每 1 秒回调）
- `ResultValidator` — 三种断言类型（status_code / response_time / response_body JSON Path）
- `PerformanceExecutor` — 主执行器
  - `execute()` 入口：Token 获取 → 并发执行 → 指标采集 → 断言验证
  - 使用 `HttpClient`（`app.executors.http_client`）获取 Token
  - 三种模式：
    - **concurrent**：固定并发数，持续 duration 秒
    - **qps**：固定 QPS 限流，持续 duration 秒
    - **step**：阶梯并发，每 step_duration 秒递增 step_concurrency
  - 通过 `asyncio.Event` 实现优雅停止
  - 预热阶段（ramp_up）逐步增加并发

**运行模式（Windows 兼容）**：
- 引擎运行在独立线程中
- `asyncio.set_event_loop(None)` → `new_event_loop()` → `run_until_complete(executor.execute())`
- 复用 `playwright_executor.py` 已验证的 Windows 兼容模式

#### 4. Service 层 (`backend/app/services/performance_service.py`)

**CRUD 操作**：
- `create_task()` / `get_task()` / `list_tasks()` / `update_task()` / `delete_task()`

**执行控制**：
- `run_task()` — 启动执行（`_run_sync` 线程池模式）
- `stop_task()` — 发送停止信号（`asyncio.Event.set()`）
- `_run_sync()` — 线程内执行完整流程（登录 → 执行 → 保存快照/结果 → 更新状态）
- `_running_executors: Dict[str, PerformanceExecutor]` 类缓存跟踪运行中任务

**数据查询**：
- `get_snapshots()` / `get_report()` / `get_task_results()`

**认证配置**：
```python
_DEFAULT_BASE_URL = "https://192.168.5.44"
_DEFAULT_ACCOUNTS = {
    "superadmin": {"username": "admin@xty", "password": "Ubains@13579"},
    "admin": {"username": "admin@xty", "password": "Ubains@13579"},
    "user": {"username": "admin@xty", "password": "Ubains@13579"},
}
```
- 验证码固定 `csba`
- 认证路径：`/platform/api/auth/login`
- 验证码路径：`/platform/api/code`
- Token 通过 `HttpClient.login(account_key)` 获取 → 注入请求头

#### 5. API 路由 (`backend/app/routers/performance.py`)

前缀 `/api/performance`，注册于 `main.py` 第 216-219 行，tags `["性能测试"]`

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/performance/tasks` | 任务列表（分页 + 状态过滤） |
| POST | `/api/performance/tasks` | 创建任务 |
| GET | `/api/performance/tasks/{task_id}` | 获取单个任务 |
| PUT | `/api/performance/tasks/{task_id}` | 更新任务 |
| DELETE | `/api/performance/tasks/{task_id}` | 删除任务 |
| POST | `/api/performance/tasks/{task_id}/run` | 执行任务 |
| POST | `/api/performance/tasks/{task_id}/stop` | 停止任务 |
| GET | `/api/performance/tasks/{task_id}/snapshots` | 获取快照列表 |
| GET | `/api/performance/tasks/{task_id}/report` | 获取报告 |
| GET | `/api/performance/tasks/{task_id}/results` | 获取执行结果 |
| WS | `/api/performance/ws/{task_id}` | 实时监控 WebSocket |

**WebSocket**：
- 端点：`/api/performance/ws/{task_id}`
- 分组名：`perf_{task_id}`
- 复用 `app/websocket/manager.py` 的 `ConnectionManager`
- 推送格式：执行器每秒回调的 JSON 快照

#### 6. 后端注册 (`backend/app/main.py`)

```python
from app.routers import performance as performance_router
# ...
app.include_router(performance_router.router, prefix="/api/performance", tags=["性能测试"])
```

### 前端（Phase 4）

#### 1. 路由 (`frontend/src/router/index.ts`)

```
/performance → Performance index.vue (懒加载)
/cases/performance → redirect /performance
/execution/performance → redirect /performance
/reports/performance → redirect /performance
```

#### 2. 菜单 (`frontend/src/App.vue`)

三个侧边栏菜单项指向 `/performance`：
- 用例管理 → 性能测试
- 执行中心 → 性能测试
- 报告中心 → 性能测试

#### 3. 主容器 (`frontend/src/views/performance/index.vue`)

三个 el-tabs：任务管理 / 执行监控 / 报告查看
- 状态管理：`monitorTaskId`、`reportTaskId`
- Tab 切换时联动（点击"监控"→ 切换到监控 tab 并传入 taskId）
- 通过 `defineExpose` 暴露 `loadReport()` / `refresh()`

#### 4. 任务列表 (`frontend/src/views/performance/TaskList.vue`)

- el-table 展示：名称/URL/方法/模式/并发/时长/状态/TPS/错误率/创建时间
- 操作列：执行/停止/监控/报告/编辑/删除
- 状态过滤下拉框 + 分页
- 创建/编辑弹窗（el-dialog）：名称/URL/方法/模式/并发/时长/预热/QPS目标/阶梯并发/请求头/请求体/认证开关/账号选择/描述
- 表单校验：名称/URL/方法/模式 必填
- `defineEmits<{ viewMonitor: [taskId: string]; viewReport: [taskId: string] }>()`

#### 5. 实时监控 (`frontend/src/views/performance/MonitorPanel.vue`)

- WebSocket 实时连接（`createPerfMonitorStream`）
- 三张 ECharts 图表：TPS 趋势（折线图 + 面积渐变）/ 响应时间趋势（平均/P50/P90/P99 四条线）/ 状态码分布（环形饼图）
- 顶部 4 个指标卡片：当前 TPS / 平均响应时间 / 总请求数 / 错误率
- 连接状态指示器（绿/黄/灰圆点）
- 数据缓冲区上限 300 点，先进先出
- 自动断开处理 + 错误提示

#### 6. 报告查看 (`frontend/src/views/performance/ReportPanel.vue`)

- 执行摘要（el-descriptions：任务名/URL/方法/模式/并发数/账号/开始/结束/实际时长）
- 核心指标卡片（8 个）：总请求数/成功数/失败数/错误率/TPS/平均/最小/最大响应时间
- 响应时间分位数表（P50/P90/P99）
- 状态码分布表（2xx/3xx/4xx/5xx + 数量 + 占比）
- TPS 趋势图 + 响应时间趋势图（ECharts）
- 导出 JSON 功能

#### 7. API 封装 (`frontend/src/api/performance.ts`)

```typescript
BASE = '/api/performance'
listTasks(params) → GET /tasks
getTask(id) → GET /tasks/{id}
createTask(data) → POST /tasks
updateTask(id, data) → PUT /tasks/{id}
deleteTask(id) → DELETE /tasks/{id}
runTask(id) → POST /tasks/{id}/run
stopTask(id) → POST /tasks/{id}/stop
getSnapshots(id) → GET /tasks/{id}/snapshots
getReport(id) → GET /tasks/{id}/report
getTaskResults(id) → GET /tasks/{id}/results
createPerfMonitorStream(id, onSnapshot, onComplete, onError, onClose) → WebSocket
```

#### 8. 类型定义 (`frontend/src/types/performance.ts`)

所有接口类型（camelCase 风格）：
- `PerformanceTask` / `PerformanceTaskListItem` / `PerformanceTaskCreate`
- `PerformanceSnapshot` / `PerformanceSnapshotListResponse`
- `PerformanceReportResponse` / `PerformanceReportSummary` / `PerformanceReportMetrics`
- `AssertionRule`
- `PerformanceRunRequest` / `PerformanceRunResponse`

#### 9. 前端构建验证

```bash
cd frontend && npm run build
# ✓ 构建成功 (34.48s), 无 TypeScript 错误
```

---

## 错误修复记录

### 1. 后端 404 / OpenAPI 不显示性能测试路由

**现象**：访问 `http://localhost:8001/docs` 看不到性能测试 API，`/api/performance/tasks` 返回 404

**根因**：运行在 8001 端口的 uvicorn 是**旧进程**（PID 10912，启动于 2026-08-11 15:50），早于性能模块代码的编写时间。代码中的 `performance` 路由注册在 `main.py` 中，但旧进程加载的是旧代码。

**验证**：通过 Python 直接 import 确认路由注册正确：
```python
from app.main import app
routes = [r.path for r in app.routes if "/api/performance" in str(r.path)]
# 输出: 11 条路由，全部正确
```

**修复**：尝试 kill 旧进程失败（access denied / not found），改用端口 8002 启动新后端。已验证 8002 正常工作。

### 2. 无法 kill 旧 uvicorn 进程

**尝试过的方法**（全部失败）：
- `taskkill /F /PID 10912` → 访问被拒绝
- `taskkill /F /PID 27100` → 未找到进程
- PowerShell `Stop-Process` → not found
- `kill -9` → no such process
- `os.kill(pid, 9)` → WinError 5 拒绝访问
- `wmic` → not found in bash

**原因**：进程由不同上下文的用户启动（可能是通过 Windows 服务或 Administrator 启动的），当前 bash 会话无权限操作。

**当前解决**：使用端口 8002 启动新后端：
```bash
cd backend && uvicorn app.main:app --reload --port 8002
```

### 3. 前端 TS 类型错误（本会话前已修复）

- TS6133: `emit` 未使用 → 移除
- TS2339: `tasks`/`total` 找不到 → 修复类型定义
- TS2345: `string | null` 不能赋值给 `string` → 修复 `formatTime` 参数类型

### 4. A0027 时间格式不正确（2026-08-17 已修复）

**现象**：会议预约压测全部返回 `{"success":false,"code":"A0027","message":"时间格式不正确"}`，HTTP 200 导致统计显示成功但捕获为空。

**根因**：服务器 `template_resolver.py` 旧版 `_resolve_token()` 用 `token.split(":")` 分割所有 token；任务占位符 `NOW_+30_M` 无冒号 → `kind="NOW_+30_M"` 匹配失败 → 占位符原样发送。本地版本已用 `_` 分割（仅 REF 用 `:`），部署时曾发现更早版本 `_now_str` 为 `%H:%M`（无秒）——两层问题叠加。

**修复**：部署本地正确版 `template_resolver.py` 到 5.60 → 删 `*.pyc` → 重启容器。验证 `_now_str()` → `2026-08-17 12:59:09`。

**踩坑**：
- 源码改了但容器仍旧行为 → stale `__pycache__` 字节码（mtime 早于源码），必须删 pyc + 重启
- 部署方式：paramiko exec + base64（scp 因 UTF-8 内容失败；sshpass 不在 PATH）
- 5.60 容器内部端口是 **80**（不是 8001），`docker port` 查映射

### 5. A0026 超过会议室可预订的最大时长（2026-08-17 定位）

**现象**：任务 body `duration: 120` 时全部返回 A0026。

**根因**：会议室 `29dcd33aa47797e7d29d79ae6711d597` 单次预订上限 **90 分钟**。

**修复**：`duration` 改为 60（与 start/end 跨度一致）。

### 6. A0023 该时间段已有会议（2026-08-17 规避）

**现象**：任务时段与已有会议重叠时全部 A0023。

**根因**：a) 时段本身有真实会议；b) **探测脚本自己创建的会议也占用时段**（探测 + 验证共用同一会议室时极易自我冲突）。

**修复**：改用 +37H/+38H（次日次日凌晨）无占用时段。教训：探测可用性会**污染**日程，探测与正式压测要用不同时段/会议室。

---

## 当前运行状态

### 生产服务器 192.168.5.60（2026-08-17 13:45）
- 容器 `plat-auto-test-app`：healthy，内部端口 80（宿主机 80）
- 部署内容：修复版 `template_resolver.py` + 含 `_save_outputs` 的 `performance_service.py` + 含捕获的 `performance_executor.py`（DBG 调试日志已清除，grep 验证 0 残留）
- 字节码缓存已清理，容器已重启加载新代码
- 任务 `perf_5a1a3b4e00e04b2f96713f4108580d2b` 验证通过（169 请求 / 0 失败 / TPS 10.96 / message_ids 捕获 2 个）
- MySQL 表 `performance_task_outputs` 已存在且有数据

### 本地（Windows 开发机）
- 后端：`python -m uvicorn app.main:app --port 8001`（无 --reload，日志 `/tmp/backend_8001.log`）
- 前端：`http://localhost:3000`
- 本地数据库 `data/test_platform.db` 含同名任务 `perf_f27debe671d84b4fb3908af5beac67ed`（待完整回归）

---

## 未提交的代码

性能测试模块代码尚未 commit。影响文件：

```
# 后端新文件
backend/app/executors/performance_executor.py       (NEW)
backend/app/services/performance_service.py          (NEW)
backend/app/routers/performance.py                   (NEW)
backend/app/models/performance.py                    (NEW)
backend/app/schemas/performance.py                   (NEW)
backend/app/main.py                                  (MODIFIED: 注册 performance router)

# 前端新文件
frontend/src/views/performance/index.vue             (NEW)
frontend/src/views/performance/TaskList.vue          (NEW)
frontend/src/views/performance/MonitorPanel.vue      (NEW)
frontend/src/views/performance/ReportPanel.vue       (NEW)
frontend/src/api/performance.ts                      (NEW)
frontend/src/types/performance.ts                    (NEW)
frontend/src/router/index.ts                         (MODIFIED: 添加 /performance 路由)
frontend/src/App.vue                                 (MODIFIED: 添加侧边栏菜单项)
```

---

## 下一步任务

### Phase 5 - 集成测试（✅ 核心已通过，2026-08-17）

以下环节已在 5.60 服务器 + 真实会议预约 API 上验证通过：
- 创建/更新任务（PUT 更新 concurrency/duration/body）✅
- 执行任务（`{"message":"任务已开始执行"}` → completed）✅
- 捕获输出落库与查询（`/outputs` 返回 message_ids）✅
- 真实 API 认证（JWT + 签名）+ 模板占位符解析 ✅

仍待验证：
- WebSocket `/api/performance/ws/{task_id}` 实时快照推送
- 前端任务列表/监控图表/报告面板对新数据（outputs）的展示
- 本地任务 `perf_f27debe671d84b4fb3908af5beac67ed` 的完整回归

### P0 - 按请求序号错开时间段（并发压测预约类接口的关键）

**问题**：concurrent 模式下所有请求解析出相同 startTime → 第一批订走时段，后续全部 A0023。

**方案**：`template_resolver.py` 的 `_resolve_token()` 增加 `INDEX` 后缀支持，如 `{__NOW_+37_H_INDEX__}`（token parts = `["NOW","+37","H","INDEX"]`），解析时在基准时间上再加 `request_index` 分钟。实现位置：`_resolve_token` 的 NOW 分支处理 `len(parts) > 3 and parts[3] == "INDEX"`，需要新增 `_now_with_offset` 的带 index 变体。实现后任务 body 改用该语法即可全并发成功。

### P1 - 修改/取消会议压测任务

- 修改会议：`PUT /meetingV3/api/message/{...}`（需先拿 book 的 messageId → 已可通过 REF 占位符 `{__REF:task_id:message_ids__}` 引用）
- 取消会议：需探测取消接口路径与 body 格式（`meeting_test*.py` 系列脚本可扩展）

### P2 - 后续优化

- 断言规则验证（`ResultValidator` 执行中启用；注意当前 `success:false` + HTTP 200 会被 status_code 断言判成功，建议增加 response_body/JSONPath 断言 `$.success == true`）
- 更详细的错误处理（Token 过期重试、网络错误分类）
- 报告对比功能（多次执行结果对比）
- 批量导出报告
- 压力测试数据清理（定期清理旧快照；探测脚本创建的真实会议也需清理策略）
- schemas/performance.py 服务器版同步：本地 `body_template` 已放宽为 `Optional[Any]`，服务器 schema 若仍为 `Optional[Dict]` 需同步部署

### P3 - 从 curl 自动生成压测任务 ✅（已实现 + 已部署 5.60）

已实现并部署，详见本会话「#### 0. 从 curl 自动生成压测任务」节。

**2026-08-19 问题修复（curl 导入 `[object Object]`，P0）：**

1. **根因**：`CurlParseRequest` 缺少 `model_config` alias 配置，前端发送 camelCase `{curlCommand}` → 后端 422 → 前端 `Array.toString()` 显示 `[object Object]`
2. **已修复（本次会话，已部署 5.60）**：
   - `schemas/performance.py`：`CurlParseRequest` 补全 `model_config`
   - `utils/curl_parser.py`：新增 `_preprocess_cmd_style()` 兼容 Windows CMD `^` 续行/`^"` 转义
   - `utils/curl_parser.py`：新增 `-b`/`--cookie` 参数支持
   - `tests/test_curl_parser.py`：新增 4 个测试用例（15 passed）
3. **文档**：`Docs/PRD/性能测试/问题处理/_问题处理_curl导入解析失败[object Object].md` + `_执行计划_修复curl导入解析失败[object Object].md`
4. **待办**：提交代码；用用户真实 CMD curl 复验完整导入（已用 CMD 风格命令实测通过）

**2026-08-19 部署结果**：schemas/performance.py + utils/curl_parser.py 已 scp → 清理 *.pyc → 重启容器（healthy）✅
- 5.60 实测 camelCase `{curlCommand}` → 200 ✅
- 5.60 实测完整 Windows CMD curl（^ 续行 + Authorization + 签名头 + -b Cookie）→ authRequired=true / signRequest=true / cookie 有值 ✅

### P3.5 - 修复 curl 导入后保存任务 422 [object Object]（P0 已修复 + 已部署 5.60）

**问题**：curl 导入解析成功 → 点保存 → `保存失败: [object Object],[object Object],[object Object],[object Object]`（用户质疑"确定更新了吗？怎么还是新增报错？"）

**根因（3 层独立问题）**：
1. **schema 非 Optional 字段拒绝 null**：`PerformanceTaskCreate` 的 `qps_target/ramp_up/step_concurrency/step_duration` 为 `int`/`List[int]`，前端在非 step/qps 模式发 `null` → Pydantic v2 422
2. **错误信息不可读**：`e.response.data.detail` 是 Pydantic 错误对象数组，`.toString()` → 4 个 `[object Object]`
3. **stepConcurrency 数组双重包裹**：编辑回填时 `[form.stepConcurrency]` 套已有数组 → `[[10,20,30]]` 校验失败

**修复（3 文件）**：
| 文件 | 变更 |
|------|------|
| `backend/app/schemas/performance.py` | 4 字段改 `Optional` + `default=None`（`qps_target/ramp_up/step_concurrency/step_duration`） |
| `backend/app/services/performance_service.py` | `create_task()` 补齐 Optional 压测参数默认值（`100/10/[]/60`） |
| `frontend/src/views/performance/TaskList.vue` | 新增 `extractApiError()` 渲染 `字段: msg` 可读错误；`handleSave()` 修复 `stepConcurrency` 双重包裹；`handleCurlParse()` 错误处理走 `extractApiError()` |

**部署（2026-08-19 18:00 左右）**：scp schemas/performance.py + performance_service.py → 清理 *.pyc → 重启容器 healthy ✅；`npm run build`（31.34s）→ scp 前端 dist 全量 ✅
**验证**：5.60 实测带 null 的 POST /tasks → **201 创建成功**，DB 落库 `100/10/[]/60` ✅（测试任务已删除）

**⚠️ 用户侧**：浏览器需 **Ctrl+F5 强刷** 加载新前端（dist 静态文件无需重启容器，但浏览器可能缓存旧 JS）。

### P4 - 任务级自定义凭据的部署与验证 ✅（已部署）

- 第 2 批本地改动（自定义登录凭据）已部署 5.60 服务器（与 curl 导入一起，2026-08-19）
- **部署后验证（本次会话）**：新建任务填自定义账号/密码 → 执行 → 日志确认用自定义凭据登录；留空 → 回退默认（验证方式，可复用）

---

*本文档由 Claude Code 自动生成，用于多会话协作开发时的上下文传递。*