# 性能测试 API 接口预设功能 - 计划执行文档

> **文档版本**: v1.0
> **创建日期**: 2026-08-13
> **关联需求**: `_PRD_接口预设功能.md`（位于 `Docs/PRD/性能测试/需求文档/`）
> **执行策略**: 分阶段迭代，每阶段可独立验证

---

## 一、执行总览

### 1.1 阶段划分

| 阶段 | 名称 | 核心交付 | 预计工期 |
|------|------|---------|---------|
| Phase 1 | 数据模型与 Schema | ApiPreset 模型 + Pydantic Schema + 表注册 | 0.5天 |
| Phase 2 | 预设 CRUD 服务与路由 | 预设增删改查 API | 0.5天 |
| Phase 3 | 任务创建继承预设 | PerformanceTask 新增 preset_id + 后端继承逻辑 | 0.5天 |
| Phase 4 | 前端预设管理页面 | 预设列表页 + 编辑对话框 | 1天 |
| Phase 5 | 前端任务表单改造 | 预设下拉框 + 字段隐藏 + 摘要卡片 | 1天 |

### 1.2 依赖关系

```
Phase 1 (模型/Schema) ──→ Phase 2 (CRUD API) ──→ Phase 4 (预设管理页面)
                          │                          ↑
                          └──→ Phase 3 (任务继承) ──→ Phase 5 (任务表单改造)
```

### 1.3 文件清单

| 文件 | 阶段 | 说明 |
|------|------|------|
| `backend/app/models/api_preset.py` | Phase 1 | ApiPreset 新表 |
| `backend/app/schemas/api_preset.py` | Phase 1 | 预设 Pydantic Schema |
| `backend/app/database.py` | Phase 1 | 注册新表 + 补齐字段 |
| `backend/app/models/__init__.py` | Phase 1 | 导出新模型 |
| `backend/app/services/api_preset_service.py` | Phase 2 | 预设业务逻辑 |
| `backend/app/routers/api_preset.py` | Phase 2 | 预设 CRUD 路由 |
| `backend/app/main.py` | Phase 2 | 注册路由 |
| `backend/app/models/performance.py` | Phase 3 | 新增 preset_id 字段 |
| `backend/app/schemas/performance.py` | Phase 3 | 新增 preset_id 字段 |
| `backend/app/services/performance_service.py` | Phase 3 | 创建任务继承预设 |
| `frontend/src/types/performance.ts` | Phase 4 | 预设类型定义 |
| `frontend/src/api/performance.ts` | Phase 4 | 预设 API 封装 |
| `frontend/src/views/performance/ApiPresetList.vue` | Phase 4 | 预设管理列表页 |
| `frontend/src/views/performance/ApiPresetDialog.vue` | Phase 4 | 预设编辑对话框 |
| `frontend/src/router/index.ts` | Phase 4 | 新增路由 |
| `frontend/src/views/performance/TaskList.vue` | Phase 5 | 任务表单集成预设下拉框 |

---

## 二、Phase 1: 数据模型与 Schema

### 2.1 任务清单

| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 1.1 | ApiPreset 模型 | `backend/app/models/api_preset.py` | 预设存储表 |
| 1.2 | 预设 Schema | `backend/app/schemas/api_preset.py` | Create/Update/Response |
| 1.3 | 注册新表 | `backend/app/database.py` | 导入 + 注册模型 |
| 1.4 | 模型导出 | `backend/app/models/__init__.py` | 加入 __all__ |

### 2.2 详细设计

#### 2.2.1 ApiPreset 模型

```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：api_preset.py
模块描述：API 接口预设数据库模型 — 存储可复用的压测接口配置
作者：czj
创建日期：2026-08-13
"""

from datetime import datetime
from typing import Optional, Any
from sqlalchemy import String, Text, JSON, Boolean, DateTime
from sqlalchemy.orm import Mapped, mapped_column

from app.database import Base
from app.utils.id_generator import generate_id  # 或 uuid 自定义


class ApiPreset(Base):
    """
    API 接口预设

    存储一组可复用的压测接口配置（方法、URL、请求头、请求体模板、
    认证配置、捕获规则），创建性能测试任务时可选择预设自动填充。
    """
    __tablename__ = "api_presets"

    id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="唯一标识 preset_xxx")
    name: Mapped[str] = mapped_column(String(100), unique=True, comment="预设名称")
    description: Mapped[str] = mapped_column(Text, default="", comment="描述")

    # 接口配置
    method: Mapped[str] = mapped_column(String(10), default="GET", comment="HTTP方法")
    target_url: Mapped[str] = mapped_column(Text, default="", comment="目标URL")
    headers: Mapped[dict] = mapped_column(JSON, default=dict, comment="请求头")
    body: Mapped[Optional[Any]] = mapped_column(JSON, nullable=True, comment="静态请求体")
    body_template: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="请求体模板")

    # 认证配置
    auth_required: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否需要登录Token")
    sign_request: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否需要签名")
    account_key: Mapped[str] = mapped_column(String(100), default="", comment="使用账号")

    # 响应捕获
    capture_rules: Mapped[Optional[list]] = mapped_column(JSON, nullable=True, comment="捕获规则")

    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
    updated_at: Mapped[datetime] = mapped_column(
        DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
    )

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "name": self.name,
            "description": self.description,
            "method": self.method,
            "target_url": self.target_url,
            "headers": self.headers or {},
            "body": self.body,
            "body_template": self.body_template,
            "auth_required": self.auth_required,
            "sign_request": self.sign_request,
            "account_key": self.account_key,
            "capture_rules": self.capture_rules or [],
            "created_at": self.created_at.isoformat() if self.created_at else None,
            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
        }
```

#### 2.2.2 预设 Schema

```python
# schemas/api_preset.py
class ApiPresetBase(BaseModel):
    """预设基础字段"""
    model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)

    name: str = Field(..., min_length=1, max_length=100, description="预设名称")
    description: str = Field("", description="描述")
    method: str = Field("GET", description="HTTP方法 GET/POST/PUT/DELETE")
    target_url: str = Field(..., description="目标接口完整URL")
    headers: Optional[Dict[str, str]] = Field({}, description="请求头")
    body: Optional[Any] = Field(None, description="静态请求体（dict或list）")
    body_template: Optional[Dict[str, Any]] = Field(None, description="请求体模板")
    auth_required: bool = Field(False, description="是否需要登录")
    sign_request: bool = Field(False, description="是否需要签名")
    account_key: Optional[str] = Field(None, description="使用账号")
    capture_rules: Optional[List[CaptureRule]] = Field(None, description="响应捕获规则")

class ApiPresetCreate(ApiPresetBase): ...
class ApiPresetUpdate(ApiPresetBase): ...
class ApiPresetResponse(ApiPresetBase):
    id: str
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None
```

> 注意：`CaptureRule` 已定义于 `app/schemas/performance_output.py`，直接复用。

#### 2.2.3 database.py 注册

```python
# 新表由 create_all 自动创建（模型导入后注册到 Base.metadata）
from app.models import api_preset  # noqa: F401
```

---

## 三、Phase 2: 预设 CRUD 服务与路由

### 3.1 任务清单

| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 2.1 | ApiPresetService | `backend/app/services/api_preset_service.py` | 增删改查 |
| 2.2 | ApiPreset 路由 | `backend/app/routers/api_preset.py` | CRUD 端点 |
| 2.3 | 注册路由 | `backend/app/main.py` | /api/performance/presets |

### 3.2 详细设计

#### 3.2.1 服务层

```python
# services/api_preset_service.py
class ApiPresetService:
    def __init__(self, db: AsyncSession):
        self.db = db

    async def list_presets(self, page, page_size, keyword) -> (list, int):
        """分页查询，支持名称模糊搜索"""

    async def get_preset(self, preset_id) -> ApiPreset:
        """详情查询，不存在抛 404"""

    async def create_preset(self, data: ApiPresetCreate) -> ApiPreset:
        """创建，名称唯一校验"""

    async def update_preset(self, preset_id, data: ApiPresetUpdate) -> ApiPreset:
        """更新，只更新传入的非 None 字段"""

    async def delete_preset(self, preset_id) -> None:
        """删除"""
```

#### 3.2.2 路由

```python
# routers/api_preset.py
router = APIRouter()  # main.py 注册时 prefix="/api/performance"

@router.get("/presets", response_model=ApiPresetListResponse)
async def list_presets(page=1, page_size=20, keyword=None, db=Depends(get_db)):
    """预设列表（分页 + 搜索）"""

@router.post("/presets", response_model=ApiPresetResponse, status_code=201)
async def create_preset(data: ApiPresetCreate, db=Depends(get_db)):
    """创建预设"""

@router.get("/presets/{preset_id}", response_model=ApiPresetResponse)
async def get_preset(preset_id: str, db=Depends(get_db)):
    """预设详情"""

@router.put("/presets/{preset_id}", response_model=ApiPresetResponse)
async def update_preset(preset_id: str, data: ApiPresetUpdate, db=Depends(get_db)):
    """更新预设"""

@router.delete("/presets/{preset_id}", status_code=204)
async def delete_preset(preset_id: str, db=Depends(get_db)):
    """删除预设"""
```

#### 3.2.3 ID 生成

沿用项目约定 `utils/id_generator.py` 生成 `preset_xxx` 前缀 ID。

---

## 四、Phase 3: 任务创建继承预设

### 4.1 任务清单

| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 3.1 | PerformanceTask 新增 preset_id | `backend/app/models/performance.py` | 字段 |
| 3.2 | Schema 新增 preset_id | `backend/app/schemas/performance.py` | 字段 |
| 3.3 | 创建任务继承逻辑 | `backend/app/services/performance_service.py` | 预设填充 |
| 3.4 | 旧库补齐字段 | `backend/app/database.py` | columns_to_add |

### 4.2 详细设计

#### 4.2.1 模型与 Schema

```python
# models/performance.py
preset_id: Mapped[Optional[str]] = mapped_column(
    String(64), nullable=True, default=None, comment="关联的接口预设ID（可为空）"
)

# schemas/performance.py - 三个 Schema 均增加
preset_id: Optional[str] = Field(None, description="关联的接口预设ID")
```

#### 4.2.2 创建任务继承逻辑

```python
# services/performance_service.py
async def create_task(self, data: PerformanceTaskCreate) -> PerformanceTask:
    """创建性能测试任务，若指定 preset_id 则继承预设配置"""
    inherited = {}
    if data.preset_id:
        preset = await self.db.get(ApiPreset, data.preset_id)
        if not preset:
            raise HTTPException(status_code=404, detail=f"接口预设不存在: {data.preset_id}")

        # 仅当任务显式未传时才使用预设值
        inherited = {
            "method": preset.method,
            "target_url": preset.target_url,
            "headers": preset.headers or {},
            "body": preset.body,
            "body_template": preset.body_template,
            "auth_required": preset.auth_required,
            "sign_request": preset.sign_request,
            "account_key": preset.account_key,
            "capture_rules": preset.capture_rules,
        }

    # 任务显式字段优先，预设字段兜底
    params = data.model_dump(exclude_unset=True)
    resolved = {**inherited, **params}

    task = PerformanceTask(
        id=generate_id("perf"),
        preset_id=data.preset_id,
        **resolved,
    )
    self.db.add(task)
    await self.db.commit()
    await self.db.refresh(task)
    return task
```

> **关键点**：`data.model_dump(exclude_unset=True)` 只返回前端显式传入的字段。前端选择预设后不传接口字段 → `exclude_unset` 为空 → 全部用预设值；前端传了则覆盖预设。

#### 4.2.3 旧库升级

```python
# database.py columns_to_add 增加
("performance_tasks", "preset_id", "VARCHAR(64) DEFAULT NULL"),
```

---

## 五、Phase 4: 前端预设管理页面

### 5.1 任务清单

| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 4.1 | 预设类型定义 | `frontend/src/types/performance.ts` | ApiPreset 类型 |
| 4.2 | 预设 API 封装 | `frontend/src/api/performance.ts` | CRUD 函数 |
| 4.3 | 预设列表页 | `frontend/src/views/performance/ApiPresetList.vue` | 列表 + 操作 |
| 4.4 | 预设编辑对话框 | `frontend/src/views/performance/ApiPresetDialog.vue` | 编辑表单 |
| 4.5 | 路由注册 | `frontend/src/router/index.ts` | /performance/presets |

### 5.2 详细设计

#### 5.2.1 类型定义

```typescript
// types/performance.ts
export interface ApiPreset {
  id: string
  name: string
  description: string
  method: string
  targetUrl: string
  headers?: Record<string, string>
  body?: any
  bodyTemplate?: any
  authRequired: boolean
  signRequest: boolean
  accountKey?: string
  captureRules?: CaptureRule[]
  createdAt?: string
  updatedAt?: string
}

export interface ApiPresetCreate {
  name: string
  description?: string
  method: string
  targetUrl: string
  headers?: Record<string, string>
  body?: any
  bodyTemplate?: any
  authRequired: boolean
  signRequest: boolean
  accountKey?: string
  captureRules?: CaptureRule[]
}

export interface ApiPresetListResponse {
  total: number
  items: ApiPreset[]
}
```

#### 5.2.2 API 封装

```typescript
// api/performance.ts
export const listPresets = (params = {}) =>
  request.get('/api/performance/presets', { params })
export const createPreset = (data: ApiPresetCreate) =>
  request.post('/api/performance/presets', data)
export const updatePreset = (id: string, data: Partial<ApiPresetCreate>) =>
  request.put(`/api/performance/presets/${id}`, data)
export const deletePreset = (id: string) =>
  request.delete(`/api/performance/presets/${id}`)
```

#### 5.2.3 预设列表页

```
┌─ 接口预设管理 ───────────────────────────────┐
│  [+ 新建预设]  [搜索框]                       │
│ ┌──────────────────────────────────────────┐ │
│ │ 名称       │ 方法  │ URL摘要     │ 更新时间 │ 操作 │
│ │ 新建会议V3 │ PUT   │ /meetingV3/ │ 08-13  │ 编辑/删除 │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```

- 表格列：名称、方法、URL 摘要（截断显示）、更新时间、操作
- 删除需 `ElMessageBox.confirm` 二次确认，提示"删除预设不影响已有任务"

#### 5.2.4 预设编辑对话框

```
┌─ 编辑接口预设 ─────────────────────────────┐
│ 名称*         [新建会议V3          ]        │
│ 描述          [会议模块压测用       ]        │
│ HTTP方法*     [PUT ▾]                      │
│ 目标URL*      [https://192.168.5.44/... ]  │
│ 请求头(JSON)  [{"Content-Type": "...  }  ] │
│ 请求体类型    (○ 静态  ● 模板)           │
│ 请求体模板    [{ "__NOW__", ...      }    ] │
│    [插入占位符 ▾]                         │
│ 认证开关      [需要登录] [需要签名] [账号▾]  │
│ 响应捕获规则  [+ 添加规则]                 │
│   key | jsonPath | 多值 | 描述 | 操作      │
│ ───────────────────────────────────────── │
│              [取消]      [保存]            │
└───────────────────────────────────────────┘
```

- 表单字段与 bodyTemplate PRD 的配置页设计保持一致（占位符按钮、捕获规则表格）
- HTTP 方法下拉：GET/POST/PUT/DELETE
- 保存时校验：名称、URL 必填；body 或 bodyTemplate 解析为合法 JSON

---

## 六、Phase 5: 前端任务表单改造

### 5.1 任务清单

| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 5.1 | 表单增加预设下拉框 | `TaskList.vue` | Promise select |
| 5.2 | 预设继承填充 + 字段隐藏 | `TaskList.vue` | 联动逻辑 |
| 5.3 | 只读摘要卡片 | `TaskList.vue` | 接口信息展示 |

### 5.2 详细设计

#### 5.2.1 表单结构变化

**改造前**（现有）：

```
名称 | 方法 | URL | 请求头 | 请求体(模板) | 认证 | 压测参数 | 断言
```

**改造后**：

```
┌─ 接口配置 ──────────────────────────────┐
│ 接口预设   [无预设 ▾ | 新建会议V3 ▾ ...] │   ← 下拉选择
│ (选择预设后显示摘要卡片，隐藏方法/URL等)   │
└────────────────────────────────────────┘

┌─ 压测参数 ──────────────────────────────┐
│ 名称 | 模式 | 并发 | 时长 | QPS/阶梯 | 断言 │
└────────────────────────────────────────┘
```

#### 5.2.2 预设下拉框逻辑

```typescript
// 预设下拉数据
const presets = ref<ApiPreset[]>([])
const selectedPreset = ref<string>('')

// 选择预设后
function onPresetChange(presetId: string) {
  if (!presetId) {
    // 无预设 → 恢复原表单
    showPresetOnly = false
    return
  }
  const preset = presets.value.find(p => p.id === presetId)
  // 填充接口字段
  form.method = preset.method
  form.targetUrl = preset.targetUrl
  form.headers = JSON.stringify(preset.headers || {}, null, 2)
  if (preset.bodyTemplate) {
    form.bodyType = 'template'
    form.bodyTemplate = JSON.stringify(preset.bodyTemplate, null, 2)
  } else if (preset.body) {
    form.bodyType = 'static'
    form.body = JSON.stringify(preset.body, null, 2)
  }
  form.authRequired = preset.authRequired
  form.signRequest = preset.signRequest
  form.accountKey = preset.accountKey
  form.captureRules = preset.captureRules || []
  // 隐藏接口字段，显示摘要
  showPresetOnly = true
}
```

#### 5.2.3 提交逻辑

```typescript
async function handleSave() {
  // 选择预设时提交 preset_id
  // 接口字段已填充到 form，仍会提交（后端已有值，覆盖无关紧要）
  const payload = {
    ...formData,
    ...(selectedPreset.value ? { presetId: selectedPreset.value } : {}),
  }
  if (editId.value) {
    await updateTask(editId.value, payload)
  } else {
    await createTask(payload)
  }
}
```

#### 5.2.4 编辑已有任务

- 编辑任务时若 `task.presetId` 存在，预设下拉框回显该预设
- 用户切换预设 → 重新填充接口字段
- 选择"无预设" → 保留当前已填充的字段不变（用户可以继续微调）

---

## 七、验证方案

### 7.1 后端单元验证

| # | 场景 | 步骤 | 预期 |
|---|------|------|------|
| 1 | 创建预设 | POST /api/performance/presets | 201，返回带 ID 的预设 |
| 2 | 预设列表 | GET /api/performance/presets | 返回创建的预设 |
| 3 | 更新预设 | PUT /api/performance/presets/{id} | 200，字段更新 |
| 4 | 删除预设 | DELETE /api/performance/presets/{id} | 204 |
| 5 | 名称重复 | 创建同名预设 | 409/400 校验错误 |
| 6 | 创建任务继承 | POST /tasks 带 preset_id | 接口字段来自预设 |
| 7 | 任务字段覆盖 | POST /tasks 带 preset_id + 显式 method | method 用显式值 |
| 8 | 无预设兼容 | POST /tasks 不带 preset_id | 行为与原来一致 |

### 7.2 前端验证

| # | 场景 | 步骤 | 预期 |
|---|------|------|------|
| 1 | 预设管理 CRUD | 页面操作 | 列表/编辑/删除正常 |
| 2 | 选择预设建任务 | 下拉选"新建会议V3" | 接口字段自动填充并隐藏 |
| 3 | 摘要卡片 | 选定预设后 | 显示方法+URL+认证摘要 |
| 4 | 无预设 | 下拉选"无预设" | 显示全部接口字段 |
| 5 | 预设建任务执行 | 选预设 → 保存 → 执行 | 压测成功 |
| 6 | 编辑带预设任务 | 打开带 presetId 任务 | 预设回显 |

### 7.3 真实场景（集成）

1. 在预设页创建"新建会议V3"预设（PUT + bodyTemplate + 捕获 messageIds + 登录 + 签名）
2. 新建压测任务 → 选择该预设 → 仅填压测参数 → 保存 → 执行
3. 断言执行成功且 `message_ids` 被捕获查询得到

---

## 八、风险与应对

| 风险 | 影响 | 应对 |
|------|------|------|
| 预设字段与任务字段不映射 | 继承逻辑漏字段 | 以 Create/Update Schema 为准逐一核对 |
| 前端隐藏字段后无法微调 | 用户灵活性下降 | 提供"无预设"选项 + 摘要卡片展示全部细节 |
| 预设名称重复歧义 | 同名预设无法区分 | name unique 约束 + 409 错误提示 |
| 预设更新影响历史任务 | 历史压测数据失真 | 任务存储快照值，不引用预设 |
| body 为数组类型 | 静态请求体可能是 list | body 用 Optional[Any]，前端 JSON.parse 支持 |

---

## 九、验收标准

见 PRD 第七节。核心验收：

1. 用户可在界面维护接口预设（增删改查）
2. 新建任务时下拉选择预设即可自动填充接口信息，无需手动输入
3. 选择预设后接口字段隐藏，界面简洁
4. 已有任务创建流程不受影响（"无预设"选项存在）
5. 使用预设创建的任务可正常执行

---

*本文档供 prd-code skill 执行使用，按 Phase 顺序逐步实现。*