# 两种压测场景增强 - 计划执行文档

> **关联 PRD**：`_PRD_两种压测场景增强需求文档.md`
> **文档版本**：v1.1
> **制定日期**：2026-08-22
> **执行分支**：`platform-auto-test`
> **优先级**：P1

> **v1.1 变更**：新建弹窗改为双类型选择（瞬时并发 / 长稳压测）；长稳升级为多接口混合负载模型（scenario_apis 字段）。

---

## 一、执行目标

实现两种新型压测场景：

1. **瞬时并发（Burst）**：集合点同步屏障 + 循环次数控制 + Ramp-Up 0 秒
2. **长稳压测（Endurance - 多接口混合负载）**：多接口导入 + 每接口独立线程分配 + 思考时间 + CSV 参数化 + 8 小时调度器 + 接口维度指标分解

包括以下变更：
- 后端模型新增 `scenario_type` / `scenario_apis` 等 15 个字段
- 执行引擎新增 `_run_burst` / `_run_endurance` 方法（含集合点、多接口线程组、思考时间、CSV 参数化）
- 前端新建弹窗重构为**类型选择→场景化表单**流程
- 存量任务（concurrent/qps/step）完全兼容

---

## 二、实施原则

1. **先文档后代码**：本计划与 PRD 完成后再实施代码。
2. **双类型入口**：新建弹窗先选类型（瞬时并发 / 长稳压测），再展示对应表单，旧表单留给存量任务编辑。
3. **兼容优先**：存量任务 `scenario_type=null`，走原路径完全不变。
4. **`_ensure_columns` 迁移**：沿用项目已有的自动列补充模式。
5. **curl 为主流程**：瞬时并发导入单条 curl；长稳批量导入（前端逐条调用 parse-curl API）。

---

## 三、阶段计划

### Phase 0：文档准备

| 工作项 | 文件 | 结果 |
|--------|------|------|
| 编写需求文档 | `_PRD_两种压测场景增强需求文档.md` | 明确范围、参数设计、UI 设计、验收标准 |
| 编写本计划 | `_PRD_两种压测场景增强_计划执行.md` | 明确代码阶段、文件变更、验证方式 |

### Phase 1：后端模型与 Schema（+ 数据库迁移）

#### 1.1 Model 新增字段

文件：`backend/app/models/performance.py`

在 `PerformanceTask` 类中新增以下字段（放在 `# 压测配置` 区域，`step_duration` 之后）：

```python
# 场景类型（null=存量标准模式，burst=瞬时并发，endurance=长稳压测）
scenario_type: Mapped[Optional[str]] = mapped_column(
    String(20), nullable=True, default=None, comment="场景类型: burst/endurance"
)
# 长稳接口列表（[{"name":"","method":"","url":"","headers":{},"body":null,"thread_count":10,...}]）
scenario_apis: Mapped[Optional[list]] = mapped_column(
    JSON, nullable=True, default=None, comment="长稳接口列表，每接口独立配置线程数"
)

# 循环控制
loop_type: Mapped[str] = mapped_column(
    String(10), default="finite", comment="循环类型: finite(有限次)/infinite(无限)"
)
loop_count: Mapped[int] = mapped_column(Integer, default=1, comment="有限循环次数")

# 集合点（Synchronizing Timer）
synchronizing_timer_enabled: Mapped[bool] = mapped_column(
    Boolean, default=False, comment="是否启用集合点"
)
synchronizing_timer_count: Mapped[int] = mapped_column(
    Integer, default=0, comment="集合点阈值"
)
synchronizing_timer_timeout: Mapped[int] = mapped_column(
    Integer, default=30000, comment="集合点超时(ms)"
)

# 错误动作
error_action: Mapped[str] = mapped_column(
    String(10), default="continue", comment="错误动作: continue/stop"
)

# 思考时间（Think Time）
think_time_enabled: Mapped[bool] = mapped_column(
    Boolean, default=False, comment="是否启用思考时间"
)
think_time_min: Mapped[int] = mapped_column(
    Integer, default=1000, comment="均值/固定值(ms)"
)
think_time_max: Mapped[int] = mapped_column(
    Integer, default=3000, comment="gaussian偏差上限(ms)"
)
think_time_distribution: Mapped[str] = mapped_column(
    String(10), default="constant", comment="分布类型: constant/gaussian"
)

# CSV 参数化
csv_parameterization_enabled: Mapped[bool] = mapped_column(
    Boolean, default=False, comment="是否启用CSV参数化"
)
csv_content: Mapped[str] = mapped_column(Text, default="", comment="CSV文件内容")
csv_variable_mapping: Mapped[dict] = mapped_column(
    JSON, default=dict, comment="CSV列名→变量名映射"
)
```

在 `to_dict()` 中新增对应字段序列化；新增 `api_summary` 字段（与 `transaction_summary` 并列，JSON 类型，nullable=True）。

#### 1.2 Schema 新增字段

文件：`backend/app/schemas/performance.py`

**新增类型定义：**

```python
class ScenarioApiConfig(BaseModel):
    """长稳接口配置"""
    name: str = Field(..., min_length=1, max_length=200, description="接口名称")
    method: str = Field("GET", description="HTTP方法")
    url: str = Field(..., description="接口URL")
    headers: Dict[str, str] = Field(default_factory=dict, description="请求头")
    body: Optional[Any] = Field(None, description="请求体")
    thread_count: int = Field(10, ge=1, le=1000, description="分配线程数")
    assertions: List[AssertionRule] = Field(default_factory=list, description="断言规则")
    unique_fields: Optional[List[UniqueFieldRule]] = Field(None, description="唯一性字段配置")

    model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
```

**`PerformanceTaskCreate`** 新增字段：

```python
# 场景类型
scenario_type: Optional[str] = Field(None, description="场景类型: burst/endurance（null=标准模式）")
scenario_apis: Optional[List[ScenarioApiConfig]] = Field(None, description="长稳接口列表")

# 循环控制
loop_type: str = Field("finite", pattern=r"^(finite|infinite)$", description="循环类型")
loop_count: int = Field(1, ge=1, le=9999, description="有限循环次数")

# 集合点
synchronizing_timer_enabled: bool = Field(False, description="是否启用集合点")
synchronizing_timer_count: int = Field(0, ge=0, le=10000, description="集合点阈值")
synchronizing_timer_timeout: int = Field(30000, ge=1000, le=120000, description="集合点超时(ms)")

# 错误动作
error_action: str = Field("continue", pattern=r"^(continue|stop)$", description="错误动作")

# 思考时间
think_time_enabled: bool = Field(False, description="是否启用思考时间")
think_time_min: int = Field(1000, ge=0, le=60000, description="均值/固定值(ms)")
think_time_max: int = Field(3000, ge=0, le=60000, description="gaussian偏差上限(ms)")
think_time_distribution: str = Field("constant", pattern=r"^(constant|gaussian)$", description="分布类型")

# CSV 参数化
csv_parameterization_enabled: bool = Field(False, description="是否启用CSV参数化")
csv_content: str = Field("", description="CSV文件内容")
csv_variable_mapping: Dict[str, str] = Field(default_factory=dict, description="CSV列名→变量名映射")
```

**现有字段约束变更：**

```python
concurrency: int = Field(10, ge=1, le=1000, description="并发数")  # 500→1000
duration: int = Field(60, ge=1, le=28800, description="压测时长(秒)")  # 3600→28800
```

**`PerformanceTaskUpdate`** — 同步新增所有可选字段。

**`PerformanceTaskResponse`** — 同步新增所有字段 + `api_summary`。

#### 1.3 数据库自动迁移

文件：`backend/app/services/performance_service.py`

在 `_ensure_columns` 方法中新增所有列的迁移逻辑。

#### 1.4 路由层

文件：`backend/app/routers/performance.py`

**无需修改路由**。CRUD 复用现有端点，新字段在 payload 中自动生效。

### Phase 2：执行引擎增强

#### 2.1 execute() 入口分支

文件：`backend/app/executors/performance_executor.py`

在 `execute()` 方法中，增加场景类型判断：

```python
scenario = getattr(task, "scenario_type", None)
if scenario == "burst":
    result = await self._run_burst(task)
elif scenario == "endurance":
    result = await self._run_endurance(task)
else:
    # 现有 concurrent/qps/step 逻辑完全不变
    ...
```

#### 2.2 新增 `_run_burst`（集合点同步屏障）

```python
async def _run_burst(self, task) -> dict:
    """
    瞬时并发模式：集合点同步屏障 + 循环次数控制

    所有线程到达集合点后同时发出请求，适用于 Spike Test。
    """
    barrier_count = task.synchronizing_timer_count or task.concurrency
    # 使用 asyncio.Barrier（Python 3.11+ 内置 timeout）
    barrier = asyncio.Barrier(barrier_count, timeout=task.synchronizing_timer_timeout / 1000)

    connector = aiohttp.TCPConnector(limit=task.concurrency, limit_per_host=task.concurrency, ssl=False)
    sampler_task = asyncio.create_task(self._snapshot_loop(task))

    try:
        async with aiohttp.ClientSession(connector=connector) as session:
            workers = []
            for i in range(task.concurrency):
                workers.append(
                    asyncio.create_task(self._burst_worker(session, task, barrier, i))
                )
            # 等待所有线程执行完毕
            await asyncio.gather(*workers, return_exceptions=True)
    finally:
        self._running = False
        sampler_task.cancel()
        try:
            await sampler_task
        except asyncio.CancelledError:
            pass

    return self._metrics.summary()
```

```python
async def _burst_worker(self, session, task, barrier, thread_index):
    """瞬时并发工作协程：集合点等待 → 齐发 → 循环"""

    semaphore = asyncio.Semaphore(task.concurrency)
    loop_count = task.loop_count if task.loop_type == "finite" else 1

    for _ in range(loop_count):
        if not self._running:
            break
        try:
            # 等待集合点（所有线程在此同步）
            await barrier.wait()
        except asyncio.BrokenBarrierError:
            # 超时导致 barrier broken，已到达线程继续
            pass
        except Exception:
            if task.error_action == "stop":
                self._running = False
            return

        # 所有线程同时发出请求
        async with semaphore:
            try:
                await self._send_request(session, task, thread_index)
            except Exception:
                if task.error_action == "stop":
                    self._running = False
                    return
```

#### 2.3 新增 `_run_endurance`（多接口混合负载）

```python
async def _run_endurance(self, task) -> dict:
    """
    长稳压测模式：多接口混合负载 + 思考时间 + CSV 参数化

    每个接口独立线程组，各自循环发送请求，共同构成混合负载。
    """
    apis = getattr(task, "scenario_apis", None) or []
    if not apis:
        return {"error": True, "error_message": "长稳压测至少需要一个接口"}

    total_concurrency = sum(api.get("thread_count", 10) for api in apis)
    connector = aiohttp.TCPConnector(limit=total_concurrency, limit_per_host=total_concurrency, ssl=False)
    sampler_task = asyncio.create_task(self._snapshot_loop(task))

    # 解析 CSV 参数化
    csv_rows = self._parse_csv(task)

    try:
        async with aiohttp.ClientSession(connector=connector) as session:
            semaphore = asyncio.Semaphore(total_concurrency)
            workers = []
            global_thread_index = 0

            for api_config in apis:
                api_name = api_config.get("name", "unknown")
                api_threads = api_config.get("thread_count", 10)
                for i in range(api_threads):
                    workers.append(
                        asyncio.create_task(
                            self._endurance_worker(
                                session, task, semaphore, api_config,
                                global_thread_index, csv_rows
                            )
                        )
                    )
                    global_thread_index += 1

            # Ramp-Up：按总线程数均匀间隔启动
            if task.ramp_up > 0 and total_concurrency > 0:
                ramp_start = time.time()
                for w in workers:
                    if not self._running:
                        break
                    # 启动协程（已创建，但 await sleep 控制启动节奏）
                    await asyncio.sleep(task.ramp_up / total_concurrency)
                logger.info(f"长稳预热完成: {len(workers)} 线程, 耗时 {time.time()-ramp_start:.1f}s")

            # 调度器时长
            await self._sleep_with_check(task.duration)
            self._running = False

            await asyncio.gather(*workers, return_exceptions=True)
    finally:
        sampler_task.cancel()
        try:
            await sampler_task
        except asyncio.CancelledError:
            pass

    # 汇总接口维度指标
    result = self._metrics.summary()
    result["api_summary"] = self._build_api_summary()
    return result
```

```python
async def _endurance_worker(self, session, task, semaphore, api_config,
                             global_thread_index, csv_rows):
    """长稳工作协程：无限循环 + 思考时间 + CSV 参数"""

    # 提取本接口配置
    api_name = api_config.get("name", "unknown")
    api_url = api_config.get("url", "")
    api_method = api_config.get("method", "GET")
    api_headers = api_config.get("headers", None) or {}
    api_body = api_config.get("body", None)

    # 分配 CSV 参数（按全局线程索引取模）
    thread_params = {}
    if csv_rows:
        row = csv_rows[global_thread_index % len(csv_rows)]
        thread_params = row

    # 构造本线程专用的请求上下文
    # 注入 CSV 变量到请求配置
    resolved_url = self._resolve_csv_vars(api_url, thread_params)
    resolved_headers = {
        k: self._resolve_csv_vars(v, thread_params)
        for k, v in api_headers.items()
    }

    while self._running:
        async with semaphore:
            try:
                await self._send_request(
                    session, task, global_thread_index,
                    override_url=resolved_url,
                    override_method=api_method,
                    override_headers=resolved_headers,
                    override_body=api_body,
                    api_name=api_name,
                )
            except Exception:
                if task.error_action == "stop":
                    self._running = False
                    break

        # 思考时间
        if task.think_time_enabled:
            await self._think_time(task)
```

#### 2.4 新增辅助方法

```python
@staticmethod
async def _think_time(task) -> None:
    """思考时间延迟"""
    import random
    if task.think_time_distribution == "gaussian":
        mean = task.think_time_min
        std = (task.think_time_max - task.think_time_min) / 3
        delay = random.gauss(mean, std)
        delay = max(0, min(delay, task.think_time_max * 2))
    else:
        delay = task.think_time_min
    await asyncio.sleep(delay / 1000)


@staticmethod
def _parse_csv(task) -> List[Dict[str, str]]:
    """解析 CSV 内容为行字典列表"""
    if not task.csv_parameterization_enabled or not task.csv_content:
        return []
    import csv, io
    reader = csv.DictReader(io.StringIO(task.csv_content))
    mapping = task.csv_variable_mapping or {}
    result = []
    for row in reader:
        mapped = {}
        for csv_col, var_name in mapping.items():
            if csv_col in row:
                mapped[var_name] = row[csv_col]
        result.append(mapped)
    return result


@staticmethod
def _resolve_csv_vars(text: str, params: dict) -> str:
    """替换文本中的 {{变量名}} 为 CSV 参数值"""
    if not params or not text:
        return text
    for key, value in params.items():
        text = text.replace("{{" + key + "}}", value)
    return text
```

#### 2.5 `_send_request` 方法扩展

现有 `_send_request` 方法增加可选参数 `override_url`、`override_method`、`override_headers`、`override_body`、`api_name`，使其可被 endurance worker 复用：

```python
async def _send_request(self, session, task, request_index=0,
                        override_url=None, override_method=None,
                        override_headers=None, override_body=None,
                        api_name=None):
    # 使用 override 参数替代 task 的默认配置
    url = override_url or task.target_url
    method = override_method or task.method
    headers = override_headers or (task.headers or {})
    body = override_body if override_body is not None else task.body
    ...
    # 记录时携带 api_name 标签
    # 在 MetricsCollector 中按 api_name 分流统计
```

#### 2.6 MetricsCollector 扩展（接口维度）

在 `MetricsCollector` 中增加 `api_stats` 字典，按 `api_name` 分流统计：

```python
# 新增
self._api_stats: Dict[str, dict] = {}  # api_name → {count, success, fail, times, ...}

def record(self, ..., api_name: str = ""):
    ...
    # 接口维度统计
    if api_name:
        if api_name not in self._api_stats:
            self._api_stats[api_name] = {"total": 0, "success": 0, "fail": 0,
                                          "times": [], "min": 0, "max": 0, ...}
        stat = self._api_stats[api_name]
        stat["total"] += 1
        ...
```

新增 `_build_api_summary()` 方法汇总接口维度指标。

### Phase 3：前端 UI 改造

#### 3.1 TypeScript 类型扩展

文件：`frontend/src/types/performance.ts`

```typescript
/** 压测场景类型 */
export type PerfScenario = 'burst' | 'endurance' | null  // null=存量标准模式

/** 长稳接口配置 */
export interface ScenarioApiConfig {
  name: string
  method: string
  url: string
  headers?: Record<string, string> | null
  body?: any | null
  threadCount: number
  assertions?: AssertionRule[]
  uniqueFields?: UniqueFieldRule[] | null
}

/** 性能测试任务（扩展） */
export interface PerformanceTask {
  // ... 现有字段
  scenarioType?: PerfScenario
  scenarioApis?: ScenarioApiConfig[] | null
  loopType?: 'finite' | 'infinite'
  loopCount?: number
  synchronizingTimerEnabled?: boolean
  synchronizingTimerCount?: number
  synchronizingTimerTimeout?: number
  errorAction?: 'continue' | 'stop'
  thinkTimeEnabled?: boolean
  thinkTimeMin?: number
  thinkTimeMax?: number
  thinkTimeDistribution?: 'constant' | 'gaussian'
  csvParameterizationEnabled?: boolean
  csvContent?: string
  csvVariableMapping?: Record<string, string>
  apiSummary?: any | null
}
```

#### 3.2 TaskList.vue 重构

文件：`frontend/src/views/performance/TaskList.vue`

**3.2.1 新建弹窗第一步：类型选择**

```vue
<template>
  <!-- 存量任务编辑：使用旧表单 -->
  <el-dialog v-if="isEditMode && !form.scenarioType" ...>
    ... 现有旧表单 ...
  </el-dialog>

  <!-- 新建任务：先选类型 -->
  <el-dialog v-else-if="dialogVisible && !selectedType" title="选择压测类型" ...>
    <div class="scenario-selector">
      <div class="scenario-card" @click="selectType('burst')">
        <div class="scenario-icon">⚡</div>
        <div class="scenario-name">瞬时并发</div>
        <div class="scenario-desc">海量用户同一瞬间齐发请求，验证系统流量尖峰承受能力</div>
        <div class="scenario-tags">集合点齐发·单接口</div>
      </div>
      <div class="scenario-card" @click="selectType('endurance')">
        <div class="scenario-icon">⏱</div>
        <div class="scenario-name">长稳压测</div>
        <div class="scenario-desc">多接口混合负载持续压测 8 小时，检测内存泄漏/性能退化</div>
        <div class="scenario-tags">多接口·每接口独立线程·思考时间</div>
      </div>
    </div>
  </el-dialog>

  <!-- 选完类型后：场景化表单 -->
  <el-dialog v-else-if="dialogVisible && selectedType" :title="`${typeLabel} - 任务配置`" width="800px" top="2vh" ...>
    <el-form ...>
      <!-- 顶部：类型切换 -->
      <el-form-item>
        <el-tag :type="selectedType === 'burst' ? 'warning' : 'primary'">{{ typeLabel }}</el-tag>
        <el-button text @click="switchType">切换类型</el-button>
      </el-form-item>

      <!-- 基础配置 -->
      <el-row :gutter="16">
        <el-col :span="12">
          <el-form-item label="任务名称" prop="name">...</el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="归属项目" prop="projectId">...</el-form-item>
        </el-col>
      </el-row>

      <!-- 瞬时并发表单 -->
      <burst-form v-if="selectedType === 'burst'" v-model="form" />

      <!-- 长稳压测表单 -->
      <endurance-form v-else-if="selectedType === 'endurance'" v-model="form" />
    </el-form>
  </el-dialog>
</template>
```

**3.2.2 子组件：`BurstForm.vue`**（瞬时并发参数组）

```vue
<template>
  <el-divider content-position="left">📥 接口配置</el-divider>
  <el-button @click="importCurl" :loading="parsingCurl">从 curl 导入</el-button>

  <el-row :gutter="16">
    <el-col :span="6">
      <el-form-item label="Method" prop="method">
        <el-select v-model="form.method" style="width:100%">...</el-select>
      </el-form-item>
    </el-col>
    <el-col :span="18">
      <el-form-item label="URL" prop="targetUrl">
        <el-input v-model="form.targetUrl" placeholder="https://..." />
      </el-form-item>
    </el-col>
  </el-row>
  <el-row :gutter="16">
    <el-col :span="12">
      <el-form-item label="请求头(JSON)" prop="headers">
        <el-input v-model="form.headers" type="textarea" :rows="2" />
      </el-form-item>
    </el-col>
    <el-col :span="12">
      <el-form-item label="请求体(JSON)" prop="body">
        <el-input v-model="form.body" type="textarea" :rows="2" placeholder='{"key": "value"}' />
      </el-form-item>
    </el-col>
  </el-row>

  <el-divider content-position="left">⚡ 瞬时并发配置</el-divider>

  <el-row :gutter="16">
    <el-col :span="8">
      <el-form-item label="线程数" prop="concurrency">
        <el-input-number v-model="form.concurrency" :min="1" :max="1000" style="width:100%" />
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item label="Ramp-Up(s)" prop="rampUp">
        <el-input-number v-model="form.rampUp" :min="0" :max="300" style="width:100%" />
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item label="循环次数" prop="loopCount">
        <el-input-number v-model="form.loopCount" :min="1" :max="9999" style="width:100%" />
      </el-form-item>
    </el-col>
  </el-row>

  <el-form-item>
    <el-checkbox v-model="form.synchronizingTimerEnabled">
      启用集合点（所有线程到齐后同时发出请求）
    </el-checkbox>
  </el-form-item>

  <template v-if="form.synchronizingTimerEnabled">
    <el-row :gutter="16">
      <el-col :span="12">
        <el-form-item label="集合点阈值" prop="synchronizingTimerCount">
          <el-input-number v-model="form.synchronizingTimerCount" :min="1" :max="10000" style="width:100%" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="超时时间(ms)" prop="synchronizingTimerTimeout">
          <el-input-number v-model="form.synchronizingTimerTimeout" :min="1000" :max="120000" :step="1000" style="width:100%" />
        </el-form-item>
      </el-col>
    </el-row>
  </template>

  <el-form-item label="错误动作">
    <el-radio-group v-model="form.errorAction">
      <el-radio value="continue">继续（Continue）</el-radio>
      <el-radio value="stop">停止（Stop）</el-radio>
    </el-radio-group>
  </el-form-item>
</template>
```

**3.2.3 子组件：`EnduranceForm.vue`**（多接口混合负载）

```vue
<template>
  <el-divider content-position="left">📥 接口列表</el-divider>
  <div class="api-toolbar">
    <el-button @click="batchImportCurl" :loading="parsingCurl">批量导入 curl</el-button>
    <el-button @click="addApi">+ 手动添加接口</el-button>
  </div>

  <div class="api-list">
    <div v-for="(api, idx) in form.scenarioApis" :key="idx" class="api-item">
      <div class="api-item-header">
        <span class="api-index">{{ idx + 1 }}</span>
        <el-input v-model="api.name" placeholder="接口名称" style="width:160px" />
        <el-select v-model="api.method" style="width:100px">
          <el-option label="GET" value="GET" />
          <el-option label="POST" value="POST" />
          <el-option label="PUT" value="PUT" />
          <el-option label="DELETE" value="DELETE" />
        </el-select>
        <el-input v-model="api.url" placeholder="URL" class="api-url" />
        <el-form-item label="线程" label-width="40px" class="thread-input">
          <el-input-number v-model="api.threadCount" :min="1" :max="1000" style="width:120px" />
        </el-form-item>
        <el-button text @click="expandApi(idx)">{{ api._expanded ? '收起' : '展开' }}</el-button>
        <el-button text type="danger" @click="removeApi(idx)">删除</el-button>
      </div>
      <template v-if="api._expanded">
        <div class="api-item-detail">
          <el-row :gutter="16">
            <el-col :span="12">
              <el-form-item label="请求头(JSON)">
                <el-input v-model="api.headersStr" type="textarea" :rows="2" placeholder='{"Content-Type": "application/json"}' />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="请求体(JSON)">
                <el-input v-model="api.bodyStr" type="textarea" :rows="2" placeholder='{"key": "value"}' />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="16">
            <el-col :span="12">
              <el-form-item label="断言">
                <el-button text @click="editAssertions(idx)">编辑断言</el-button>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="唯一字段">
                <el-button text @click="editUniqueFields(idx)">编辑唯一字段</el-button>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
      </template>
    </div>
  </div>

  <div class="total-concurrency">
    总线程数: <strong>{{ totalConcurrency }}</strong>（自动汇总 = Σ 各接口线程数）
  </div>

  <el-divider content-position="left">⏱ 调度配置</el-divider>

  <el-row :gutter="16">
    <el-col :span="8">
      <el-form-item label="Ramp-Up(s)" prop="rampUp">
        <el-input-number v-model="form.rampUp" :min="0" :max="300" style="width:100%" />
      </el-form-item>
    </el-col>
    <el-col :span="16">
      <el-form-item label="调度器时长(s)" prop="duration">
        <el-input-number v-model="form.duration" :min="1" :max="28800" style="width:200px" />
        <span class="duration-hint">≈ {{ (form.duration / 3600).toFixed(1) }} 小时</span>
      </el-form-item>
    </el-col>
  </el-row>

  <el-divider content-position="left">🤔 思考时间（模拟真实用户操作间隔）</el-divider>

  <el-form-item>
    <el-checkbox v-model="form.thinkTimeEnabled">启用思考时间</el-checkbox>
  </el-form-item>

  <template v-if="form.thinkTimeEnabled">
    <el-row :gutter="16">
      <el-col :span="8">
        <el-form-item label="分布类型">
          <el-radio-group v-model="form.thinkTimeDistribution">
            <el-radio value="constant">固定</el-radio>
            <el-radio value="gaussian">高斯随机</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-col>
      <el-col :span="8">
        <el-form-item label="均值(ms)" prop="thinkTimeMin">
          <el-input-number v-model="form.thinkTimeMin" :min="0" :max="60000" :step="100" style="width:100%" />
        </el-form-item>
      </el-col>
      <el-col :span="8" v-if="form.thinkTimeDistribution === 'gaussian'">
        <el-form-item label="偏差上限(ms)" prop="thinkTimeMax">
          <el-input-number v-model="form.thinkTimeMax" :min="0" :max="60000" :step="100" style="width:100%" />
        </el-form-item>
      </el-col>
    </el-row>
  </template>

  <el-divider content-position="left">📊 CSV 参数化（每线程独立身份）</el-divider>

  <el-form-item>
    <el-checkbox v-model="form.csvParameterizationEnabled">启用 CSV 参数化</el-checkbox>
  </el-form-item>

  <template v-if="form.csvParameterizationEnabled">
    <el-form-item label="CSV 内容">
      <el-input v-model="form.csvContent" type="textarea" :rows="4"
        placeholder="username,password&#10;user1,pass1&#10;user2,pass2" />
    </el-form-item>
    <el-form-item label="变量映射(JSON)">
      <el-input v-model="form.csvVariableMappingStr" type="textarea" :rows="2"
        placeholder='{"username":"user","password":"pass"}' />
    </el-form-item>
    <el-alert v-if="totalConcurrency > 0 && form.csvContent"
      :title="`CSV 行数不足时，线程将循环复用账号`" type="info" show-icon :closable="false" />
  </template>

  <el-form-item label="错误动作">
    <el-radio-group v-model="form.errorAction">
      <el-radio value="continue">继续（Continue）</el-radio>
      <el-radio value="stop">停止（Stop）</el-radio>
    </el-radio-group>
  </el-form-item>
</template>
```

**3.2.4 defaultForm() 扩展**

```typescript
const defaultForm = () => ({
  // 现有字段
  name: '',
  projectId: '',
  method: 'GET',
  targetUrl: '',
  headers: '',
  body: '',
  ...
  // 场景类型
  scenarioType: null as PerfScenario,
  scenarioApis: [] as ScenarioApiFormItem[],
  // 循环控制
  loopType: 'finite',
  loopCount: 1,
  // 集合点
  synchronizingTimerEnabled: false,
  synchronizingTimerCount: 0,
  synchronizingTimerTimeout: 30000,
  // 错误动作
  errorAction: 'continue',
  // 思考时间
  thinkTimeEnabled: false,
  thinkTimeMin: 1000,
  thinkTimeMax: 3000,
  thinkTimeDistribution: 'constant',
  // CSV
  csvParameterizationEnabled: false,
  csvContent: '',
  csvVariableMappingStr: '{}',
})
```

**3.2.5 场景预设填充**

```typescript
function selectType(type: 'burst' | 'endurance') {
  selectedType.value = type
  form.scenarioType = type

  if (type === 'burst') {
    // 瞬时并发预设
    form.rampUp = 0
    form.loopType = 'finite'
    form.loopCount = 1
    form.synchronizingTimerEnabled = true
    form.synchronizingTimerCount = form.concurrency
    form.synchronizingTimerTimeout = 30000
    form.errorAction = 'continue'
    form.thinkTimeEnabled = false
    form.csvParameterizationEnabled = false
    form.duration = 60  // 后端 default
  } else if (type === 'endurance') {
    // 长稳压测预设
    form.duration = 28800
    form.rampUp = 120
    form.loopType = 'infinite'
    form.thinkTimeEnabled = true
    form.thinkTimeMin = 1000
    form.thinkTimeMax = 3000
    form.thinkTimeDistribution = 'gaussian'
    form.csvParameterizationEnabled = false
    form.errorAction = 'continue'
    form.synchronizingTimerEnabled = false
    form.scenarioApis = []
  }
}
```

**3.2.6 handleSave() 适配**

```typescript
async function handleSave() {
  const payload: any = {
    name: form.name,
    projectId: form.projectId || undefined,
    authRequired: form.authRequired,
    accountKey: form.accountKey,
    signRequest: form.signRequest,
    assertions: form.assertions,
  }

  if (form.scenarioType === 'burst') {
    payload.scenarioType = 'burst'
    payload.method = form.method
    payload.targetUrl = form.targetUrl
    payload.headers = parseJsonField(form.headers)
    payload.body = parseJsonField(form.body)
    payload.concurrency = form.concurrency
    payload.rampUp = form.rampUp
    payload.loopType = 'finite'
    payload.loopCount = form.loopCount
    payload.synchronizingTimerEnabled = form.synchronizingTimerEnabled
    payload.synchronizingTimerCount = form.synchronizingTimerCount
    payload.synchronizingTimerTimeout = form.synchronizingTimerTimeout
    payload.errorAction = form.errorAction
    payload.duration = 60
  } else if (form.scenarioType === 'endurance') {
    payload.scenarioType = 'endurance'
    payload.scenarioApis = form.scenarioApis.map(api => ({
      name: api.name,
      method: api.method,
      url: api.url,
      headers: parseJsonField(api.headersStr),
      body: parseJsonField(api.bodyStr),
      threadCount: api.threadCount,
      assertions: api.assertions || [],
      uniqueFields: api.uniqueFields || [],
    }))
    payload.concurrency = totalConcurrency.value
    payload.rampUp = form.rampUp
    payload.duration = form.duration
    payload.loopType = 'infinite'
    payload.thinkTimeEnabled = form.thinkTimeEnabled
    payload.thinkTimeMin = form.thinkTimeMin
    payload.thinkTimeMax = form.thinkTimeMax
    payload.thinkTimeDistribution = form.thinkTimeDistribution
    payload.csvParameterizationEnabled = form.csvParameterizationEnabled
    payload.csvContent = form.csvContent
    payload.csvVariableMapping = parseJsonField(form.csvVariableMappingStr)
    payload.errorAction = form.errorAction
    // 单接口字段给默认值
    payload.targetUrl = form.scenarioApis[0]?.url || ''
    payload.method = form.scenarioApis[0]?.method || 'GET'
  } else {
    // 存量标准模式
    payload.mode = form.mode
    ...
  }

  await saveTask(payload)
}
```

**3.2.7 编辑回填**

```typescript
function openEditDialog(row) {
  getTaskDetail(row.id).then(data => {
    if (data.scenarioType === 'burst') {
      form.scenarioType = 'burst'
      selectedType.value = 'burst'
      // 回填 burst 字段
      form.concurrency = data.concurrency
      form.rampUp = data.rampUp ?? 0
      form.loopCount = data.loopCount ?? 1
      form.synchronizingTimerEnabled = data.synchronizingTimerEnabled ?? false
      form.synchronizingTimerCount = data.synchronizingTimerCount ?? data.concurrency
      form.synchronizingTimerTimeout = data.synchronizingTimerTimeout ?? 30000
      form.errorAction = data.errorAction ?? 'continue'
      ...
    } else if (data.scenarioType === 'endurance') {
      form.scenarioType = 'endurance'
      selectedType.value = 'endurance'
      form.scenarioApis = (data.scenarioApis || []).map(api => ({
        ...api,
        _expanded: false,
        headersStr: JSON.stringify(api.headers || {}, null, 2),
        bodyStr: JSON.stringify(api.body || null, null, 2),
      }))
      ...
    } else {
      // 存量任务，使用旧表单
      form.scenarioType = null
      selectedType.value = null
      ...
    }
  })
}
```

#### 3.3 curl 批量导入

文件：`frontend/src/views/performance/TaskList.vue`

```typescript
async function batchImportCurl() {
  // 打开批量导入弹窗（textarea，多行 curl）
  // 对每行/每块逐条调用 parseCurl API
  // 每条解析结果生成一个接口条目添加到 form.scenarioApis
  // name 自动取 URL 末段路径
  // threadCount 默认 10
}
```

#### 3.4 API 封装

文件：`frontend/src/api/performance.ts`

无需新增 API 接口，复用现有 CRUD 端点。新字段随 payload 一起发送。

### Phase 4：验证与收尾

#### 4.1 后端验证

1. 启动后端，确认 `_ensure_columns` 自动添加新列
2. 创建 burst 任务（POST /api/performance/tasks），确认字段持久化正确
3. 创建 endurance 任务（含 scenario_apis），确认接口列表持久化正确
4. 存量任务查询，确认无新字段时返回默认值/null
5. 现有 concurrent/qps/step 创建流程不变

#### 4.2 执行引擎验证

1. 创建 burst 任务（100 并发，集合点启用），执行确认首秒 TPS 尖峰（所有线程同时发出）
2. 创建 endurance 任务（2 个接口，线程 10+10，思考时间 500ms constant），执行确认请求间有延迟
3. CSV 参数化任务，确认不同线程使用不同参数
4. 现有 concurrent 任务，确认执行结果与之前一致

#### 4.3 前端验证

1. `npm run build` 通过
2. 新建弹窗 → 类型选择卡片展示两种类型
3. 选择 burst → 显示 burst 表单，预设填充正确
4. 选择 endurance → 显示 endurance 表单，接口列表编辑器可用
5. 批量导入 curl → 逐条生成接口条目
6. 手动添加/删除接口，线程数自动汇总
7. 创建 burst 任务保存成功
8. 创建 endurance 任务保存成功
9. 编辑 burst/endurance 任务回填正确
10. 编辑存量任务展示旧表单

#### 4.4 兼容性验证

1. 存量任务列表/编辑/执行/报告正常
2. 批量执行含新场景任务正常排队执行
3. 合并报告不受影响

---

## 四、文件变更清单

### 新增

| 文件 | 说明 |
|------|------|
| `Docs/PRD/性能测试/需求文档/_PRD_两种压测场景增强需求文档.md` | 需求文档 |
| `Docs/PRD/性能测试/需求文档/_PRD_两种压测场景增强_计划执行.md` | 本执行计划 |
| `frontend/src/views/performance/BurstForm.vue` | 瞬时并发表单子组件 |
| `frontend/src/views/performance/EnduranceForm.vue` | 长稳压测表单子组件 |

### 修改

| 文件 | 变更 |
|------|------|
| `backend/app/models/performance.py` | 新增 15 个字段 + to_dict() 序列化 + api_summary 字段 |
| `backend/app/schemas/performance.py` | 新增 ScenarioApiConfig Schema + 所有新字段 |
| `backend/app/executors/performance_executor.py` | 新增 `_run_burst`、`_run_endurance`、`_burst_worker`、`_endurance_worker`、`_think_time`、`_parse_csv`、`_resolve_csv_vars` + MetricsCollector 接口维度扩展 + `_build_api_summary` |
| `backend/app/services/performance_service.py` | `_ensure_columns` 新增列迁移 |
| `frontend/src/types/performance.ts` | 新增 PerfScenario、ScenarioApiConfig 类型 + 接口扩展 |
| `frontend/src/views/performance/TaskList.vue` | 类型选择流程 + 场景化表单切换 + 批量导入 curl + handleSave 适配 |

### 明确不修改

- `backend/app/database.py` — 数据库连接配置不变
- `backend/app/routers/performance.py` — 路由不变（复用现有 CRUD）
- `backend/app/main.py` — 入口不变
- 前端其他页面（MonitorPanel、ReportPanel、Projects 等）— 不受影响

---

## 五、风险与应对

| 风险 | 应对 |
|------|------|
| `asyncio.Barrier` 在 Python < 3.11 无 timeout 参数 | 使用 `asyncio.Condition` + `wait_for` 自实现 barrier；或确认项目 Python 版本 ≥ 3.11 |
| 长稳多接口线程组启动顺序不均衡 | Ramp-Up 按总线程数均匀交错启动各接口线程 |
| 8 小时快照 28800 条，图表渲染卡顿 | 前端降采样渲染（最大 500 点）；后端存储不变 |
| CSV 与登录认证冲突（不同账号需各自登录） | v1 版本：CSV 变量仅注入请求配置，登录 Token 仍用统一账号；每线程独立登录为后续扩展 |
| 表单弹窗内容过长 | 接口条目默认折叠；思考时间/CSV 不启用时收起；使用子组件分离 |
| 存量任务编辑时误触场景类型 | 编辑存量任务直接显示旧表单，不出现类型选择入口 |
| 长稳中单接口失败影响其他接口 | 默认 error_action=continue，失败线程继续执行不影响其他接口线程组 |

---

## 六、验收门禁

代码合并前必须满足：

- [ ] PRD 与计划执行文档已落盘
- [ ] 后端模型 + Schema 新增字段通过类型检查
- [ ] `_ensure_columns` 自动迁移新增列
- [ ] 类型选择卡片展示两种类型，选择后表单切换
- [ ] Burst 任务创建/执行/查看正常（集合点同步生效）
- [ ] Endurance 任务（多接口）创建/执行/查看正常
- [ ] 接口维度指标分解在报告中展示
- [ ] 思考时间延迟生效
- [ ] CSV 参数化生效
- [ ] 批量导入 curl 生成接口条目
- [ ] 存量 concurrent/qps/step 任务不受影响
- [ ] `npm run build` 通过
- [ ] HANDOFF 文档更新进度

---

*本计划执行完成后，应同步更新性能测试 HANDOFF 文档中的 P1 进度。*