# 执行计划：批量执行后自动跳转任务监控页

> 生成时间：2026-08-21
> 关联 PRD：`_PRD_批量执行跳转监控页.md`
> 关联文档：`HANDOFF_性能测试.md`、`_PRD_性能测试项目管理与批量执行.md`

---

## 问题概述

当前「执行全部」和「批量执行」按钮点击后，API 同步阻塞等待所有任务串行完成才返回，前端长时间无响应。用户无法实时查看任务执行情况，体验差。

**核心改动**：
1. 批量执行 API 改为异步非阻塞（立即返回 `batch_id`）
2. 新增批量执行详情 API 和批量监控页面
3. 前端按钮跳转到批量监控页，自动切换显示当前任务的实时指标

---

## 执行步骤

### Step 1: 后端 Schema — BatchRunResponse 新增 task_ids 字段

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

**改动**：`BatchRunResponse` 类新增 `task_ids: List[str]` 字段

```python
class BatchRunResponse(BaseModel):
    """批量执行响应"""
    message: str
    batch_id: str
    task_count: int
    task_ids: List[str] = Field(default_factory=list, description="执行的任务ID列表")
    report_id: Optional[str] = Field(None, description="合并报告ID（批量执行完成后生成）")
```

**验收标准**：
- `BatchRunResponse` 包含 `task_ids` 字段
- 已有字段不受影响

---

### Step 2: 后端 Schema — 新增批量执行详情响应类型

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

**改动**：新增 `BatchTaskItem` 和 `BatchDetailResponse` 类

```python
class BatchTaskItem(BaseModel):
    """批量执行中的单个任务详情"""
    id: str = Field(..., description="任务ID")
    name: str = Field("", description="任务名称")
    status: str = Field("pending", description="任务状态")
    actual_tps: Optional[float] = None
    avg_response_time: Optional[float] = None
    error_rate: Optional[float] = None
    actual_concurrency: Optional[int] = None
    duration_actual: Optional[float] = None

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

class BatchDetailResponse(BaseModel):
    """批量执行详情响应"""
    batch_id: str
    project_id: Optional[str] = None
    status: str
    total_count: int
    success_count: int
    fail_count: int
    started_at: Optional[str] = None
    tasks: List[BatchTaskItem] = Field(default_factory=list)

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

**验收标准**：
- 新增类型定义正确，包含 camelCase 别名
- 与现有类型无冲突

---

### Step 3: 后端路由 — 批量执行改为异步非阻塞

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

**改动**：
1. `batch_run_tasks` 路由中，使用 `asyncio.create_task()` 在后台执行，立即返回 `batch_id` 和 `task_ids`
2. `run_project_all` 同上

**修改后代码**：

```python
@router.post("/tasks/batch-run", response_model=BatchRunResponse)
async def batch_run_tasks(
    data: BatchRunRequest,
    service: PerformanceService = Depends(get_perf_service),
):
    """批量执行指定任务列表（非阻塞，后台执行）"""
    if len(data.task_ids) == 0:
        raise HTTPException(status_code=400, detail="任务列表不能为空")

    # 立即创建批量执行记录并获取 batch_id
    batch_id = service.create_batch_record(data.task_ids, data.project_id)
    
    # 后台执行
    asyncio.create_task(service.batch_run_tasks_async(
        task_ids=data.task_ids,
        project_id=data.project_id,
        batch_id=batch_id,
    ))

    return BatchRunResponse(
        message="批量执行已启动",
        batch_id=batch_id,
        task_count=len(data.task_ids),
        task_ids=data.task_ids,
        report_id=None,
    )
```

**验收标准**：
- API 在 200ms 内返回（不等待任务执行完毕）
- 后台任务正确执行所有任务
- 批量执行完成后仍能生成合并报告

---

### Step 4: 后端 — 新增 `create_batch_record` 和 `batch_run_tasks_async` 方法

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

**改动**：

1. 新增 `create_batch_record()` 方法 — 创建批量执行记录并返回 batch_id
2. 将 `batch_run_tasks` 的方法体抽取为 `batch_run_tasks_async`（异步执行）
3. 新增 `get_batch_details()` 方法 — 查询批量执行详情

**关键代码示意**：

```python
def create_batch_record(self, task_ids: List[str], project_id: Optional[str] = None) -> str:
    """创建批量执行记录，立即返回 batch_id"""
    batch_id = generate_id("batch")
    # 创建记录...
    return batch_id

async def batch_run_tasks_async(self, task_ids, project_id, batch_id):
    """后台异步执行批量任务（独立事务，不依赖 API 请求上下文）"""
    # 需要在独立事务中执行，创建独立的数据库会话
    pass

async def get_batch_details(self, batch_id: str) -> Optional[Dict]:
    """获取批量执行详情（含各任务最新状态）"""
    pass
```

**注意**：后台异步执行时，需要创建独立的数据库会话（`async_session_maker()`），因为 `run_in_executor` 线程池中的任务完成后回写到 `performance_tasks` 表，而 API 的 session 可能在后台任务执行期间被关闭。

---

### Step 5: 后端路由 — 新增批量执行详情 API

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

**改动**：新增 `GET /batch-details/{batch_id}` 路由

```python
@router.get("/batch-details/{batch_id}", response_model=BatchDetailResponse)
async def get_batch_details(
    batch_id: str,
    service: PerformanceService = Depends(get_perf_service),
):
    """获取批量执行详情（含各任务状态）"""
    details = await service.get_batch_details(batch_id)
    if not details:
        raise HTTPException(status_code=404, detail="批量执行记录不存在")
    return details
```

**验收标准**：
- `GET /api/performance/batch-details/batch_xxx` 返回正确的批量执行详情
- 包含各任务的当前状态、TPS、响应时间等
- 批量执行进行中返回实时状态

---

### Step 6: 前端类型定义 — 新增 BatchRunResponse 字段

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

**改动**：`BatchRunResponse` 新增 `taskIds` 字段

```typescript
export interface BatchRunResponse {
  message: string
  batchId: string
  taskCount: number
  taskIds: string[]           // 新增
  reportId: string | null
}
```

**验收标准**：TypeScript 编译通过，无类型错误

---

### Step 7: 前端 API — 新增 getBatchDetails 函数

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

**改动**：在 `// ==================== 批量执行 ====================` 区域新增

```typescript
/** 获取批量执行详情 */
export function getBatchDetails(batchId: string): Promise<BatchDetailResponse> {
  return request.get(`${BASE}/batch-details/${batchId}`)
}
```

**验收标准**：API 调用正常，返回数据格式正确

---

### Step 8: 前端类型定义 — 新增批量执行详情类型

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

**改动**：新增 `BatchTaskItem` 和 `BatchDetailResponse` 接口

```typescript
export interface BatchTaskItem {
  id: string
  name: string
  status: string
  actualTps: number | null
  avgResponseTime: number | null
  errorRate: number | null
  actualConcurrency: number | null
  durationActual: number | null
}

export interface BatchDetailResponse {
  batchId: string
  projectId: string | null
  status: string
  totalCount: number
  successCount: number
  failCount: number
  startedAt: string | null
  tasks: BatchTaskItem[]
}
```

**验收标准**：TypeScript 编译通过

---

### Step 9: 前端路由 — 新增 batch-monitor 路由

**文件**：`frontend/src/router/index.ts`

**改动**：在 `performance` 子路由中新增

```typescript
{
  path: 'batch-monitor',
  name: 'PerfBatchMonitor',
  component: () => import('@/views/performance/BatchMonitor.vue'),
  meta: { title: '批量执行监控' }
}
```

**验收标准**：路由注册成功，访问 `/performance/batch-monitor?batchId=xxx` 进入页面

---

### Step 10: 前端 — 创建 BatchMonitor.vue 批量监控页面

**文件**：`frontend/src/views/performance/BatchMonitor.vue`（**NEW**）

**组件结构**：

```
BatchMonitor.vue
├── 顶部：返回按钮 + 项目名称 + 进度条
├── 任务列表表格（el-table，带颜色标记状态）
│   ├── 运行中 → 高亮行
│   ├── 已完成 → 显示 TPS/响应时间/错误率/操作（查看报告）
│   └── 等待中 → 灰色
├── 实时指标卡片（6 个，复用 MonitorPanel.vue 样式）
├── TPS 趋势图（ECharts）
├── 响应时间趋势图（含 P95）
├── 状态码分布图
├── 延迟与连接时间趋势图
└── 并发数趋势图
```

**核心逻辑**：

```typescript
// 1. 挂载时从路由 query 获取 batchId 和 projectId
const batchId = computed(() => route.query.batchId as string)
const projectId = computed(() => route.query.projectId as string)

// 2. 加载批量执行详情
async function loadBatchDetail() {
  const detail = await getBatchDetails(batchId.value)
  tasks.value = detail.tasks
  // 更新进度、成功数、失败数
}

// 3. 连接批量进度 WebSocket（ws/batch-progress）
//    接收 perf_batch_progress 消息，更新任务列表状态

// 4. 自动连接当前 running 任务的监控 WebSocket
//    监听 perf_snapshot 和 perf_complete 消息
//    当前任务完成 → 自动切换到下一个任务的 WebSocket

// 5. 定时轮询批量详情（每 2 秒，作为 WebSocket 的补充）
//    获取各任务的最新状态（completed 任务的 TPS 等指标）
```

**WebSocket 自动切换逻辑**：

```
1. 收到 perf_batch_progress → current_index 更新
2. 如果 current_task_id 变化 → 关闭旧 WebSocket，连接新 task_id 的监控 WebSocket
3. 收到 perf_complete → 标记该任务完成，更新任务列表行
4. 收到 perf_batch_progress → current_status = completed/partially_failed → 全部完成
5. 全部完成 → 停止轮询，展示「查看合并报告」按钮
```

**验收标准**：
- 页面加载后显示任务列表和进度条
- 当前运行中的任务实时显示 TPS 图表
- 任务完成后自动切换到下一个任务
- 全部完成后可查看合并报告
- 返回按钮回到项目详情页

---

### Step 11: 前端 — ProjectDetail.vue 修改执行全部跳转

**文件**：`frontend/src/views/performance/ProjectDetail.vue`

**改动**：`handleRunAll()` 和 `handleBatchRun()` 改为跳转批量监控页

**handleRunAll 修改后**：

```typescript
async function handleRunAll() {
  // ... 确认弹窗 ...
  try {
    const res = await runProjectAll(projectId.value)
    // 立即跳转到批量监控页
    router.push(`/performance/batch-monitor?batchId=${res.batchId}&projectId=${projectId.value}`)
  } catch (e: any) {
    ElMessage.error('执行失败: ' + (e.message || ''))
  }
}
```

**handleBatchRun 修改后**：

```typescript
async function handleBatchRun() {
  // ... 确认弹窗 ...
  try {
    const res = await batchRunTasks({ taskIds: selectedIds.value, projectId: projectId.value })
    // 立即跳转到批量监控页
    router.push(`/performance/batch-monitor?batchId=${res.batchId}&projectId=${projectId.value}`)
  } catch (e: any) {
    ElMessage.error('批量执行失败: ' + (e.message || ''))
  }
}
```

**不再需要**：
- `batchProgressVisible` 相关代码（进度条移至 BatchMonitor 页面）
- `batchRunId` 和 `lastReportId` 相关逻辑
- `connectBatchWs()` 和 `showBatchProgress()` 方法
- 批量执行相关的 WebSocket 代码

**验收标准**：
- 点击「执行全部」→ 确认弹窗 → API 调用 → 立即跳转到批量监控页
- 点击「批量执行（N）」→ 确认弹窗 → API 调用 → 立即跳转到批量监控页
- 旧版「查看合并报告」和「历史报告」按钮保留

---

### Step 12: 前端 — Projects.vue 修改执行全部跳转

**文件**：`frontend/src/views/performance/Projects.vue`

**改动**：`runProject()` 方法改为跳转批量监控页

```typescript
const runProject = async (project: PerformanceProject) => {
  // ... 确认弹窗 ...
  try {
    const res = await runProjectAll(project.id)
    router.push(`/performance/batch-monitor?batchId=${res.batchId}&projectId=${project.id}`)
  } catch (error: any) {
    ElMessage.error('执行失败: ' + (error.response?.data?.detail || error.message || ''))
  }
}
```

**注意**：`Projects.vue` 的「执行全部」按钮在项目卡片上，用户点击后直接跳转到批量监控页，不再停留在项目管理页等待。

**验收标准**：项目管理页点击「执行全部」→ 跳转到批量监控页

---

### Step 13: 构建前端

```bash
cd frontend && npm run build
```

**验收标准**：构建成功，无 TypeScript 错误

---

### Step 14: 部署到 5.60 服务器

```bash
scp -r frontend/dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
```

后端需重启：
```bash
ssh ubains@192.168.5.60 "sudo systemctl restart plat-auto-test"
```

**验收标准**：文件上传成功，后端重启成功

---

### Step 15: 验证

1. 浏览器 **Ctrl+F5** 强刷清除缓存
2. 进入 **性能测试 → 项目管理 → 点击项目**
3. 点击「执行全部」按钮
4. 确认：立即跳转到批量监控页，显示任务列表和进度条
5. 确认：当前任务显示实时 TPS 图表等指标
6. 确认：任务完成后自动切换到下一个任务
7. 确认：全部完成后显示「查看合并报告」按钮
8. 确认：DevTools → Network 中批量 API 在 200ms 内返回

---

## 改动清单

| 文件 | 类型 | 说明 |
|------|------|------|
| `backend/app/schemas/performance.py` | **MODIFY** | `BatchRunResponse` 新增 `task_ids`、新增 `BatchTaskItem` 和 `BatchDetailResponse` |
| `backend/app/routers/performance.py` | **MODIFY** | `batch_run_tasks`/`run_project_all` 改为异步非阻塞；新增 `GET /batch-details/{batch_id}` 路由 |
| `backend/app/services/performance_service.py` | **MODIFY** | 新增 `create_batch_record`、`batch_run_tasks_async`、`get_batch_details` 方法 |
| `frontend/src/types/performance.ts` | **MODIFY** | `BatchRunResponse` 新增 `taskIds`；新增 `BatchTaskItem`、`BatchDetailResponse` 接口 |
| `frontend/src/api/performance.ts` | **MODIFY** | 新增 `getBatchDetails` API 函数 |
| `frontend/src/views/performance/BatchMonitor.vue` | **NEW** | 批量监控页面（任务列表 + 实时指标 + ECharts 图表 + WebSocket 自动切换） |
| `frontend/src/views/performance/ProjectDetail.vue` | **MODIFY** | 「执行全部」和「批量执行」改为跳转批量监控页；移除原地进度条代码 |
| `frontend/src/views/performance/Projects.vue` | **MODIFY** | 「执行全部」改为跳转批量监控页 |
| `frontend/src/router/index.ts` | **MODIFY** | 新增 `batch-monitor` 路由 |

---

## 关键技术要点

### 后台异步执行注意事项

```python
# 在 FastAPI 中，后台任务需要独立数据库会话
# 因为 API 请求处理完成后的 session 会被关闭

async def batch_run_tasks_async(self, task_ids, project_id, batch_id):
    """后台异步批量执行"""
    # 创建独立数据库会话
    async with async_session_maker() as db:
        service = PerformanceService(db)
        # ... 执行逻辑 ...
        await db.commit()
```

### WebSocket 自动切换逻辑

```typescript
let currentTaskWs: WebSocket | null = null

function connectTaskMonitor(taskId: string) {
  // 关闭旧连接
  if (currentTaskWs) {
    currentTaskWs.close()
    currentTaskWs = null
  }
  // 连接新任务的监控 WebSocket
  currentTaskWs = createPerfMonitorStream(taskId, onSnapshot, onComplete, onError, onClose)
}
```

### 定时轮询作为 WebSocket 补充

WebSocket 推送 `perf_batch_progress` 消息时，仅包含 `current_index` 和 `success_count` 等计数，不包含每个任务的 TPS 等详细指标。需要在消息中新增 `tasks` 字段，或通过定时轮询 `GET /batch-details/{batch_id}` 获取各任务的最新状态：

```typescript
// 每 2 秒轮询批量详情
let pollTimer: number | null = null

function startPolling() {
  pollTimer = window.setInterval(async () => {
    const detail = await getBatchDetails(batchId.value)
    tasks.value = detail.tasks  // 更新任务列表表格
  }, 2000)
}
```

**推荐方案**：WebSocket 推送 + 定时轮询结合：
- WebSocket 接收 `perf_batch_progress` 处理进度条和任务切换
- 定时轮询获取各任务详细指标更新表格

---

## 预计耗时

| 步骤 | 内容 | 预计时间 |
|------|------|---------|
| Step 1 | 后端 Schema 新增字段 | 10 min |
| Step 2 | 后端 Schema 新增类型 | 10 min |
| Step 3 | 后端路由改为异步 | 15 min |
| Step 4 | 后端 Service 新增方法 | 30 min |
| Step 5 | 后端路由新增详情 API | 10 min |
| Step 6-8 | 前端类型定义 + API | 15 min |
| Step 9 | 前端路由 | 5 min |
| Step 10 | BatchMonitor.vue 页面 | 90 min |
| Step 11 | ProjectDetail.vue 跳转 | 15 min |
| Step 12 | Projects.vue 跳转 | 10 min |
| Step 13 | 构建前端 | 5 min |
| Step 14 | 部署到 5.60 | 10 min |
| Step 15 | 验证 | 15 min |
| **合计** | | **~4 小时** |

---

*本文档由 Claude Code 生成*