# PRD - 性能测试 bodyTemplate 动态变量替换与响应捕获

> **文档版本**: v1.1
> **创建日期**: 2026-08-13
> **关联模块**: 性能测试
> **优先级**: P1
> **关联需求**: `Docs/PRD/性能测试/需求文档/_PRD_性能测试模块需求文档.md`

---

## 一、背景与目标

### 1.1 背景

当前性能测试模块已具备基本的 HTTP 压测能力（并发/QPS/阶梯模式），但在实际压测场景中暴露出两个关键问题：

1. **请求体硬编码**：body 中的参数字段（如会议名称、开始时间、结束时间）是静态值，每次请求发送完全相同的数据。对于"新建会议"这类接口，第一个请求成功创建会议后，后续请求因重复创建而全部返回 4xx 错误，导致压测结果失真。

2. **响应数据无法复用**：创建会议接口返回的 `messageId`（会议 ID）无法被后续的"取消会议"性能测试任务引用，导致相关接口无法进行有效的自动化压测。

### 1.2 目标

1. **bodyTemplate 动态变量替换**：支持在请求体模板中使用占位符变量（如 `{__NOW__}`），每次请求前自动替换为动态值，确保每次请求的 body 数据唯一有效。

2. **响应捕获与输出复用**：支持从响应体中提取关键字段（如 `messageId`），存储为任务输出，后续任务可引用这些输出作为输入参数。

3. **保持向后兼容**：已有任务（无 bodyTemplate、无响应捕获配置）执行逻辑不变，不受影响。

---

## 二、功能范围

### 2.1 核心功能

| 功能 | 描述 | 优先级 |
|------|------|--------|
| bodyTemplate 变量替换 | 支持 `{__NOW__}` 等占位符在每次请求时动态替换 | P0 |
| 响应字段捕获 | 从响应 JSON 中提取指定字段，存储为任务输出 | P0 |
| 任务输出查询 | 支持查询某次执行产生的输出数据 | P0 |
| 输出跨任务引用 | 一个任务的输出可作为另一个任务的输入变量 | P1 |
| 自定义变量扩展 | 支持用户自定义变量格式和替换规则 | P2 |

### 2.2 支持的占位符变量

| 占位符 | 说明 | 示例值 |
|--------|------|--------|
| `{__NOW__}` | 当前时间，格式 `YYYY-MM-DD HH:mm` | `2026-08-13 14:30` |
| `{__NOW_+N_H__}` | 当前时间 + N 小时 | `{__NOW_+2_H__}` → `2026-08-13 16:30` |
| `{__NOW_+N_M__}` | 当前时间 + N 分钟 | `{__NOW_+30_M__}` → `2026-08-13 15:00` |
| `{__RANDOM_NAME_N__}` | 随机名称（N 位字母数字） | `{__RANDOM_NAME_8__}` → `a3Kf9xQ2` |
| `{__UUID__}` | 随机 UUID（去横线） | `e8d4a2f17c3b4d9e8a5f6b7c8d9e0f1a` |
| `{__RANDOM_INT_N_M__}` | N 到 M 之间的随机整数 | `{__RANDOM_INT_1000_9999__}` → `5732` |
| `{__INDEX__}` | 当前请求序号（从 0 开始） | `0`, `1`, `2`, ... |
| `{__REF:task_id:key__}` | 引用其他任务的输出 | `{__REF:perf_xxx:message_ids__}` |

### 2.3 响应捕获

响应捕获规则定义在任务配置中，支持从响应 JSON 中按 JSON Path 提取字段：

```json
{
  "captureRules": [
    {
      "key": "message_ids",
      "jsonPath": "$.data.messageId",
      "multiple": true,
      "description": "捕获创建会议返回的 messageId"
    }
  ]
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `key` | string | 输出键名，用于后续引用 |
| `jsonPath` | string | JSON Path 表达式，提取目标字段 |
| `multiple` | bool | 是否允许多个值（true=追加到数组，false=覆盖） |
| `description` | string | 规则描述 |

---

## 三、详细需求

### 3.1 bodyTemplate 变量替换机制

#### 3.1.1 执行流程

```
_send_request() 调用时：
  1. 检查 task.body_template 是否存在
  2. 若存在，深拷贝 bodyTemplate 作为模板
  3. 递归遍历模板中的所有字符串值，匹配占位符模式
  4. 对每个匹配的占位符，调用对应生成器生成动态值
  5. 替换后的结果作为本次请求的 json_data
  6. 若 bodyTemplate 不存在，使用 task.body（原有逻辑）
```

#### 3.1.2 变量解析器设计

```python
class TemplateResolver:
    """
    请求体模板变量替换器
    
    支持占位符：
    - {__NOW__}           → 当前时间 "YYYY-MM-DD HH:mm"
    - {__NOW_+N_H__}      → 当前时间 + N 小时
    - {__NOW_+N_M__}      → 当前时间 + N 分钟
    - {__RANDOM_NAME_N__} → N 位随机字符串
    - {__UUID__}          → 随机 UUID（无横线）
    - {__RANDOM_INT_N_M__} → N~M 随机整数
    - {__INDEX__}         → 请求序号
    - {__REF:task_id:key__} → 引用其他任务输出
    """
    
    def resolve(self, template: dict, request_index: int) -> dict:
        """递归替换模板中的占位符，返回实际请求体"""
```

#### 3.1.3 签名适配

当使用 bodyTemplate 替换后，每次请求的 body 内容不同，签名计算必须基于替换后的实际 body，而非模板：

```python
# _send_request 中的签名逻辑调整：
json_data = self._resolve_body_template(task, request_index)  # 先替换
headers = self._build_headers(task, body_for_sign=json_data)  # 用实际 body 签名
```

### 3.2 响应捕获机制

#### 3.2.1 数据模型

```python
class PerformanceTaskOutput(Base):
    """
    性能测试任务输出
    
    记录性能测试执行过程中从响应体捕获的字段值，
    供其他任务引用（如创建会议后捕获 messageId，用于取消会议）。
    """
    __tablename__ = "performance_task_outputs"
    
    id: int           # 自增主键
    task_id: str      # 关联任务 ID
    key: str          # 输出键名（如 "message_ids"）
    value: list       # 输出值列表（JSON 数组）
    created_at: datetime
```

#### 3.2.2 捕获执行流程

```
每次请求响应后：
  1. 检查 task.capture_rules 是否存在
  2. 对每条 capture rule：
     a. 解析响应 JSON
     b. 按 jsonPath 提取字段值
     c. 若 multiple=true，追加到已有值列表
     d. 若 multiple=false，覆盖已有值
  3. 压测完成后，将最终输出写入数据库
```

#### 3.2.3 跨任务引用

当 `{__REF:task_id:key__}` 被引用时：
1. 任务启动时，从 `performance_task_outputs` 表查询 `task_id` 对应的 `key` 的输出
2. 将输出值注入到 bodyTemplate 的解析上下文中
3. 请求时按顺序或随机从值列表中取一个

### 3.3 API 扩展

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/performance/tasks/{id}/outputs` | 获取任务输出列表 |
| GET | `/api/performance/tasks/{id}/outputs/{key}` | 获取指定键的输出值 |

### 3.4 Schema 扩展

#### PerformanceTaskCreate / PerformanceTaskUpdate 新增字段

```python
body_template: Optional[Dict[str, Any]] = Field(None, description="请求体模板（支持变量替换）")
capture_rules: Optional[List[CaptureRule]] = Field(None, description="响应捕获规则列表")
```

#### CaptureRule Schema

```python
class CaptureRule(BaseModel):
    """响应捕获规则"""
    key: str = Field(..., description="输出键名")
    json_path: str = Field(..., description="JSON Path 表达式")
    multiple: bool = Field(True, description="是否允许多值")
    description: str = Field("", description="规则描述")
```

### 3.5 前端变更

#### 3.5.1 任务配置页 — bodyTemplate 编辑器

- 新增"请求体模板"编辑区域（与"请求体"互斥，二选一）
- 提供占位符插入按钮：`{__NOW__}`、`{__NOW_+2_H__}`、`{__UUID__}`、`{__RANDOM_NAME_8__}` 等
- 支持 JSON 语法高亮和校验

#### 3.5.2 任务配置页 — 响应捕获配置

- 新增"响应捕获规则"配置区域
- 支持添加/删除/编辑捕获规则
- 每项配置：key、jsonPath、是否多值

#### 3.5.3 任务详情页 — 输出查看

- 任务执行完成后，显示"执行输出"标签页
- 展示所有捕获的 key-value 列表
- 支持复制值，方便手动粘贴到其他任务配置

---

## 四、非功能需求

### 4.1 性能要求

- 变量替换对单次请求的额外耗时 < 1ms（模板解析后缓存编译结果）
- 大量响应捕获时不显著增加内存占用（限制最大捕获值数量，默认 10000 条）

### 4.2 兼容性

- 已有任务（无 bodyTemplate、无 captureRules）完全不受影响
- bodyTemplate 替换结果不影响数据库存储的原始 body 字段
- 响应捕获不改变现有指标统计逻辑

### 4.3 边界条件

| 场景 | 行为 |
|------|------|
| bodyTemplate 和 body 同时为空 | 发送无 body 请求（GET/DELETE 正常） |
| bodyTemplate 中的占位符无法解析 | 保留原样字符串，不替换 |
| 引用的 REF 任务输出为空 | 使用空字符串，记录警告日志 |
| 响应 JSON 中 jsonPath 不存在 | 跳过该条捕获规则，记录警告 |
| 捕获值超过上限 | 截断并记录警告 |

---

## 五、应用场景示例

### 5.1 新建会议压测

```json
{
  "bodyTemplate": {
    "messageName": "性能压测会议_{__RANDOM_NAME_8__}",
    "startTime": "{__NOW__}",
    "endTime": "{__NOW_+2_H__}",
    "companyNumber": "cd",
    "uid": "402881e7843e5eb101843e6b25960001",
    "configValueId": "402881e7843e5eb101843e6b25960001",
    "participantList": [
      {
        "uid": "402881e7843e5eb101843e6b25960001",
        "name": "admin",
        "department": "技术部",
        "companyNumber": "cd"
      }
    ],
    "topicList": [
      { "topicName": "性能压测议题_{__RANDOM_NAME_4__}" }
    ],
    "duration": 120
  },
  "captureRules": [
    {
      "key": "message_ids",
      "jsonPath": "$.data.messageId",
      "multiple": true,
      "description": "捕获创建会议返回的 messageId"
    }
  ]
}
```

### 5.2 取消会议压测（引用输出）

```json
{
  "bodyTemplate": {
    "mid": "{__REF:perf_create_xxx:message_ids__}"
  },
  "captureRules": []
}
```

---

## 六、影响范围

### 6.1 新增文件

| 文件 | 说明 |
|------|------|
| `backend/app/executors/template_resolver.py` | 变量替换解析器 |
| `backend/app/models/performance_output.py` | 任务输出数据库模型 |
| `backend/app/schemas/performance_output.py` | 任务输出 Pydantic Schema |
| `backend/app/routers/performance_output.py` | 任务输出 API 路由 |

### 6.2 修改文件

| 文件 | 变更 |
|------|------|
| `backend/app/executors/performance_executor.py` | `_send_request` 集成模板替换 + 响应捕获 |
| `backend/app/schemas/performance.py` | 新增 `body_template`、`capture_rules` 字段 |
| `backend/app/models/performance.py` | 新增 `capture_rules` 字段（已有 `body_template`） |
| `backend/app/services/performance_service.py` | 任务执行后保存输出 |
| `backend/app/database.py` | 注册新表 |
| `backend/app/main.py` | 注册新路由 |
| `frontend/src/api/performance.ts` | 新增 API 封装 |
| `frontend/src/types/performance.ts` | 新增类型定义 |
| `frontend/src/views/performance/*.vue` | 配置页 UI 扩展 |

---

## 七、验收标准

| # | 验收项 | 验证方式 |
|---|--------|---------|
| 1 | bodyTemplate 中的 `{__NOW__}` 被替换为当前时间 | 抓包查看实际请求 body |
| 2 | `{__NOW_+2_H__}` 正确替换为当前时间 + 2 小时 | 同上 |
| 3 | `{__RANDOM_NAME_8__}` 每次请求生成不同的 8 位随机字符串 | 连续请求查看 |
| 4 | 无 bodyTemplate 的任务执行逻辑不变 | 对比改造前后结果 |
| 5 | 响应捕获规则正确提取 messageId 并存储 | 查询 outputs API |
| 6 | multiple=true 模式下捕获多个值 | 连续请求后查看 outputs 数组长度 |
| 7 | `{__REF:task_id:key__}` 正确引用其他任务的输出 | 取消会议任务成功使用 messageId |
| 8 | 占位符无法解析时保留原样 | 配置无效占位符验证 |