# 问题处理：会议模板新增任务 body 字符串存储导致 4XX 响应

> 处理时间：2026-08-21
> 现象报告人：用户（执行会议模板新增任务时）
> 关联文档：`_执行计划_修复会议模板任务body字符串存储导致4XX响应.md`（修复计划）
> 状态：🔍 根因已确定，待修复

---

## 1. 问题现象

用户在项目管理页面，通过「会议模板」接口预设（ApiPreset）创建性能测试任务后，执行该任务时**全部请求返回 4XX 状态码**。

**问题任务**：

| 字段 | 值 |
|------|-----|
| 任务 ID | `perf_ecbd92aae3b845ab9bd651ca49ca111e` |
| 接口预设 | `preset_meeting_insertByFkeyId`（POST /meetingV3/api/template/insertByFkeyId） |
| 请求数 | 16,215 次 |
| 状态码分布 | 全部 4XX（status_4xx=16215） |
| 断言 | 无（assertions=[]），所以 fail_count=0 |
| 并发/时长 | 10 并发，持续 60s |

---

## 2. 初步定位

### 2.1 容器日志确认

查询 5.60 服务器容器日志：

```bash
docker logs plat-auto-test-app 2>&1 | grep -E '4[0-9][0-9]|status_4|4XX|status.*4' | tail -20
```

日志显示：
- 登录成功（Token 获取正常）
- 任务执行正常完成，无异常堆栈
- 只有 count 数据展示 4XX

### 2.2 数据库查询

```sql
-- 查询问题任务 body 原始存储
SELECT id, name, body, body_template, method, target_url
FROM performance_tasks
WHERE id = 'perf_ecbd92aae3b845ab9bd651ca49ca111e';
```

**结果**：`body` 列存储的内容为 **JSON 字符串**（含 `^` 转义字符），而非 JSON 对象/数组。

---

## 3. 根因分析

### 3.1 直接原因

**`_resolve_body()` 返回字符串 → `json=str` 双重序列化 → API 收到字符串字面量 → 反序列化失败 → 400/422**

执行引擎 `_send_request()` 的核心调用链：

```
_resolve_body(task, index) → 返回字符串 "[...]"
session.request(method, url, json=字符串) → aiohttp 内部 json.dumps(字符串)
→ 实际发送 HTTP body: "\"[...]\"" (带引号的字符串字面量)
→ API 期望 JSON 对象/数组，收到字符串 → 反序列化失败 → 4XX
```

### 3.2 前端根因：TaskList.vue 缺少 JSON.parse()

**问题代码**（`frontend/src/views/performance/TaskList.vue` 第 769 行）：

```typescript
const data: any = {
  // ...
  body: form.body || null,           // ❌ 未 JSON.parse，直接发送原始字符串
  bodyTemplate: form.bodyTemplate
    ? (typeof form.bodyTemplate === 'string' ? JSON.parse(form.bodyTemplate) : form.bodyTemplate)
    : null,                           // ✅ bodyTemplate 正确解析了
  // ...
}
```

**对比正确代码**（`frontend/src/views/performance/ApiPresetDialog.vue` 第 295 行）：

```typescript
data.body = JSON.parse(form.body)    // ✅ 正确解析 JSON 字符串为对象
```

`bodyTemplate` 有 `JSON.parse()` 处理，但 `body` 没有——这是**不一致的编码遗漏**。

### 3.3 数据流追踪

```
用户输入 JSON 字符串 body → 前端 form.body 为字符串
  → handleSave(): body: form.body || null  → 未 JSON.parse
    → POST /api/performance/tasks → body 字段为字符串
      → Pydantic 接收字符串 → SQLAlchemy JSON 列存储字符串
        → MySQL 存储为 JSON 字符串字面量（带外层引号）
          → 执行时 _resolve_body() 返回字符串
            → json=字符串 → aiohttp 双重序列化
              → API 收到字符串字面量 → 4XX
```

### 3.4 `^` 字符的来源

问题任务 body 中包含 `^` 字符（如 `^[{^\"messageName^\":...`），这是**Windows CMD 环境的转义字符残留**：

- `^` 在 CMD 中用于转义 `&` `|` `<` `>` 等特殊字符
- 用户可能从 CMD 窗口复制粘贴 curl 命令或 body 内容时引入了 `^`

另一个任务 `perf_82d72c5c17f94f9ebba1f899b41bc018` 的 URL 中也存在 `^&`（如 `pageNum=1^&pageSize=10`），验证了这一点。

### 3.5 为什么旧任务（8月13日前）没出问题

旧任务（如会议 book PUT）虽然 body 也是字符串，但那时**成功是因为 body 中的 `{__NOW__}` 占位符被正常替换**，且旧版本的执行引擎可能对字符串 body 做了额外处理。当前版本代码中，`_resolve_body()` 对字符串 body 不做二次解析，直接返回字符串，导致 `json=str` 双重序列化。

### 3.6 为什么没有断言失败

任务 `assertions=[]`（无断言），所有响应（包括 4XX）均被视为「成功响应」，所以 `fail_count=0`。如果任务配置了断言（如 `status == 200`），4XX 会被标记为失败，问题会更早暴露。

---

## 4. 影响范围

### 4.1 受影响的任务

通过 **TaskList.vue 页面创建**（非 ApiPresetDialog）且**使用 body** 的任务均受影响。具体判断标准：

| 条件 | 受影响 |
|------|--------|
| TaskList.vue 创建 + 有 body | ✅ 是 |
| ApiPresetDialog 创建（预设管理） | ❌ 否（有 JSON.parse） |
| GET 请求（无 body） | ❌ 否 |
| body 为 null/空 | ❌ 否 |
| 只使用 bodyTemplate | ❌ 否（bodyTemplate 有 JSON.parse） |

### 4.2 已确认受影响的任务

| 任务 ID | 名称 | URL | 问题 |
|---------|------|-----|------|
| `perf_ecbd92aae3b845ab9bd651ca49ca111e` | 会议模板新增 | POST insertByFkeyId | body 为字符串，含 `^` 字符，全部 4XX |
| `perf_82d72c5c17f94f9ebba1f899b41bc018` | 会议模板列表查询 | GET list（含 `^&`） | URL 含 `^&`，但 GET 请求无 body |

### 4.3 不涉及的范围

- **ApiPresetDialog 创建的预设**：不涉及
- **bodyTemplate 字段**：不涉及（前端已正确 JSON.parse）
- **GET 请求**：不涉及（`_resolve_body` 对 GET 返回 None）
- **后端执行引擎**：逻辑本身无 bug，只是对字符串 body 缺少防御性处理

---

## 5. 修复方案

### 5.1 前端修复（根因修复）

**`frontend/src/views/performance/TaskList.vue`** — `handleSave()` 第 769 行：

```typescript
// 修复前
body: form.body || null,

// 修复后
body: form.body ? JSON.parse(form.body) : null,
```

### 5.2 后端防御性修复

**`backend/app/executors/performance_executor.py`** — `_send_request()` 第 1018-1020 行：

```python
# 修复前：直接 json=json_data
async with session.request(method, url, headers=headers, json=json_data, ...)

# 修复后：如果 json_data 是字符串，尝试反序列化
if isinstance(json_data, str):
    try:
        json_data = json.loads(json_data)
    except (json.JSONDecodeError, TypeError):
        pass  # 非 JSON 字符串，保持原样
async with session.request(method, url, headers=headers, json=json_data, ...)
```

### 5.3 数据库脏数据修复

```sql
-- 清理 body 中被错误存储为字符串的 JSON（去除 ^ 转义字符并重新解析）
UPDATE performance_tasks
SET body = JSON_REMOVE(body, '$')
WHERE JSON_TYPE(body) = 'STRING'
  AND JSON_UNQUOTE(body) LIKE '^[%'
  AND id = 'perf_ecbd92aae3b845ab9bd651ca49ca111e';
```

---

## 6. 经验总结

1. **三端 JSON 字段一致性**：`body` 字段贯穿前端（表单输入）→ 后端（Pydantic schema）→ 数据库（JSON 列），前端提交时必须确保 `JSON.parse()`，否则整个链路断裂。

2. **编辑/创建页面代码对齐**：`TaskList.vue` 和 `ApiPresetDialog.vue` 都涉及 body 字段的编辑提交，但一处有 JSON.parse 一处没有——**同类型字段的提交逻辑应统一处理，避免复制遗漏**。

3. **Windows CMD `^` 转义**：涉及 Windows CMD 环境的数据输入，需提示用户不要从 CMD 窗口直接复制粘贴包含 `^` 的内容。

4. **断言配置的重要性**：无断言的任务即使全部 4XX 也不会标记为失败，建议在创建任务时默认添加 `status == 200` 断言。

5. **执行引擎防御性编程**：`json=str` 应该做 `json.loads(str)` 回退，防止前端错误传播到 API 请求。

---

*本文档由 Claude Code 生成*