# 执行计划 - 执行记录删除功能

> **文档类型**: 执行计划文档
> **来源文档**: `_PRD_需求文档_执行记录删除功能.md`
> **创建日期**: 2026-07-14
> **作者**: czj
> **预计工期**: 0.5 天
> **实施状态**: ✅ 已完成（2026-07-14）

---

## 一、执行概述

### 1.1 目标

为执行中心增加单个删除和批量删除执行记录的功能。

### 1.2 核心原则

**删除执行记录只删除执行结果，绝不删除测试用例本身。**

### 1.3 改动范围

| 层级 | 文件 | 改动类型 |
|------|------|----------|
| 后端服务 | `backend/app/services/execution_service.py` | 新增 delete / delete_many |
| 后端路由 | `backend/app/routers/executions.py` | 新增 DELETE + batch-delete |
| 前端 API | `frontend/src/api/executions.ts` | 新增 delete / batchDelete |
| 前端页面 | `frontend/src/views/Execution.vue` | 表格多选 + 删除按钮 + 逻辑 |

---

## 二、执行步骤

### Phase 1: 后端服务层 (预计 15 分钟)

#### Step 1.1: ExecutionService 新增删除方法

**文件**: `backend/app/services/execution_service.py`

**插入位置**: `cancel_execution` 方法之后

```python
async def delete_execution(self, execution_id: str) -> bool:
    """
    删除执行记录（级联删除关联的用例结果，不影响测试用例）

    Args:
        execution_id (str): 执行记录ID

    Returns:
        bool: 删除是否成功

    Raises:
        ValueError: 执行记录不存在或正在运行中
    """
    execution = await self.get_execution(execution_id)
    if not execution:
        raise ValueError(f"执行记录不存在: {execution_id}")

    # 运行中的执行记录禁止删除
    if execution.status == "running":
        raise ValueError(f"执行记录正在运行中，无法删除: {execution_id}")

    # 删除（级联自动删除关联的 CaseResult，不影响 TestCase）
    await self.db.delete(execution)
    await self.db.flush()
    logger.info(f"执行记录删除成功: id={execution_id}")
    return True

async def delete_executions(
    self, execution_ids: List[str]
) -> Tuple[int, int, int, List[str]]:
    """
    批量删除执行记录

    Args:
        execution_ids (List[str]): 执行记录ID列表

    Returns:
        Tuple[int, int, int, List[str]]: (成功数, 失败数, 跳过数, 错误信息列表)
    """
    success = 0
    failed = 0
    skipped = 0
    errors = []
    for eid in execution_ids:
        try:
            await self.delete_execution(eid)
            success += 1
        except ValueError as e:
            msg = str(e)
            if "运行中" in msg:
                skipped += 1
            else:
                failed += 1
                errors.append(f"{eid}: {msg}")
        except Exception as e:
            failed += 1
            errors.append(f"{eid}: {str(e)}")
    # 统一提交
    await self.db.commit()
    logger.info(f"批量删除完成: 成功={success}, 失败={failed}, 跳过={skipped}")
    return success, failed, skipped, errors
```

### Phase 2: 后端路由层 (预计 10 分钟)

#### Step 2.1: 新增删除路由

**文件**: `backend/app/routers/executions.py`

**插入位置**: 现有路由末尾

```python
@router.delete("/{execution_id}", status_code=204)
async def delete_execution(
    execution_id: str,
    service: ExecutionService = Depends(get_execution_service),
):
    """
    删除执行记录

    删除执行记录及其关联的用例结果，不影响测试用例本身。
    运行中的执行记录无法删除。

    Args:
        execution_id (str): 执行记录ID
        service (ExecutionService): 执行服务实例

    Raises:
        HTTPException: 不存在返回404，运行中返回400
    """
    try:
        deleted = await service.delete_execution(execution_id)
        if not deleted:
            raise HTTPException(status_code=404, detail=f"执行记录不存在: {execution_id}")
    except ValueError as e:
        msg = str(e)
        if "运行中" in msg:
            raise HTTPException(status_code=400, detail=msg)
        raise HTTPException(status_code=404, detail=msg)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"删除执行记录失败: {str(e)}")
        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")


@router.post("/batch-delete", response_model=dict)
async def batch_delete_executions(
    data: dict,
    service: ExecutionService = Depends(get_execution_service),
):
    """
    批量删除执行记录

    Args:
        data (dict): {"ids": ["exec_id1", "exec_id2"]}
        service (ExecutionService): 执行服务实例

    Returns:
        dict: 删除结果统计
    """
    ids = data.get("ids", [])
    if not ids:
        raise HTTPException(status_code=400, detail="未提供要删除的执行记录ID")
    try:
        success, failed, skipped, errors = await service.delete_executions(ids)
        return {
            "success_count": success,
            "failed_count": failed,
            "skipped_count": skipped,
            "errors": errors,
        }
    except Exception as e:
        logger.error(f"批量删除执行记录失败: {str(e)}")
        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
```

⚠️ **注意路由顺序**：`/batch-delete` 必须定义在 `/{execution_id}` 之前，否则 `batch-delete` 会被当作 execution_id 参数。但 FastAPI 中 `POST /batch-delete` 和 `DELETE /{execution_id}` 方法不同，路径不会冲突。为保险起见，batch-delete 放在通配路由之前。

### Phase 3: 前端 API 层 (预计 5 分钟)

#### Step 3.1: 新增 API 方法

**文件**: `frontend/src/api/executions.ts`

```typescript
/**
 * 删除执行记录（不影响测试用例）
 */
async delete(id: string): Promise<void> {
  await request.delete(`/api/executions/${id}`)
},

/**
 * 批量删除执行记录
 */
async batchDelete(ids: string[]): Promise<{
  success_count: number
  failed_count: number
  skipped_count: number
  errors: string[]
}> {
  const response = await request.post('/api/executions/batch-delete', { ids })
  return response.data
},
```

### Phase 4: 前端页面 (预计 25 分钟)

#### Step 4.1: 表格增加多选列

**文件**: `frontend/src/views/Execution.vue`

```vue
<!-- 改动前 -->
<el-table ref="tableRef" :data="executions" ...>
  <el-table-column type="expand">...</el-table-column>

<!-- 改动后 -->
<el-table
  ref="tableRef"
  :data="executions"
  ...
  @selection-change="handleSelectionChange"
>
  <!-- 多选列：运行中的记录禁用勾选 -->
  <el-table-column type="selection" width="50" :selectable="canSelect" />
  <el-table-column type="expand">...</el-table-column>
```

```typescript
// 是否可勾选（运行中的不可选）
const canSelect = (row: any) => row.status !== 'running'
// 选中的执行记录
const selectedExecutions = ref<any[]>([])
const handleSelectionChange = (selection: any[]) => {
  selectedExecutions.value = selection
}
```

#### Step 4.2: 操作栏增加批量删除按钮

```vue
<el-button
  type="danger"
  @click="batchDeleteExecutions"
  :disabled="selectedExecutions.length === 0"
>
  <el-icon><Delete /></el-icon>
  批量删除 ({{ selectedExecutions.length }})
</el-button>
```

#### Step 4.3: 操作列增加单条删除按钮

```vue
<el-button
  size="small"
  type="danger"
  link
  @click="deleteExecution(row)"
  :disabled="row.status === 'running'"
  v-if="row.status !== 'running'"
>
  删除
</el-button>
```

#### Step 4.4: 新增删除方法

```typescript
import { Delete } from '@element-plus/icons-vue'

// 单个删除
const deleteExecution = async (row: any) => {
  try {
    await ElMessageBox.confirm(
      `确定要删除执行记录 "${row.name}" 吗？删除后无法恢复（不影响测试用例）。`,
      '删除确认',
      { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
    )
    await executionApi.delete(row.id)
    ElMessage.success('删除成功')
    await loadExecutions()
  } catch (error: any) {
    if (error !== 'cancel') {
      ElMessage.error('删除失败: ' + error.message)
    }
  }
}

// 批量删除
const batchDeleteExecutions = async () => {
  if (selectedExecutions.value.length === 0) return
  try {
    await ElMessageBox.confirm(
      `确定要删除选中的 ${selectedExecutions.value.length} 条执行记录吗？删除后无法恢复（不影响测试用例）。`,
      '批量删除确认',
      { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
    )
    const ids = selectedExecutions.value.map(e => e.id)
    const result = await executionApi.batchDelete(ids)
    ElMessage.success(
      `删除完成：成功 ${result.success_count} 条` +
      (result.skipped_count > 0 ? `，跳过 ${result.skipped_count} 条（运行中）` : '')
    )
    await loadExecutions()
  } catch (error: any) {
    if (error !== 'cancel') {
      ElMessage.error('批量删除失败: ' + error.message)
    }
  }
}
```

---

### Phase 5: 功能验证 (预计 10 分钟)

#### Step 5.1: 验证清单

| 测试项 | 操作 | 预期结果 |
|--------|------|----------|
| 单个删除 | 点击某条记录的【删除】→ 确认 | ✅ 记录消失 |
| 用例完好 | 删除后查看用例管理 | ✅ 用例不受影响 |
| 批量删除 | 勾选多条 → 【批量删除】→ 确认 | ✅ 全部删除 |
| 运行中保护 | 查看运行中记录 | ✅ 无删除按钮、不可勾选 |
| 取消删除 | 弹窗点【取消】 | ✅ 不删除 |
| API 文档 | 访问 /docs | ✅ 新接口出现 |

---

## 三、风险评估

| 风险项 | 可能性 | 影响 | 缓解措施 |
|--------|--------|------|----------|
| 路由顺序冲突 | 低 | 中 | batch-delete 与动态路径方法不同，且放前 |
| 级联删除失败 | 低 | 高 | 已配置 cascade，测试验证 |
| 误删运行中记录 | 中 | 高 | 前后端双重校验（按钮禁用 + 服务层拒绝） |
| 事务未提交 | 中 | 中 | delete_executions 末尾 commit |

---

## 四、验收清单

- [x] 后端 delete_execution 方法
- [x] 后端 delete_executions 批量方法
- [x] 后端 DELETE /{id} 路由
- [x] 后端 POST /batch-delete 路由
- [x] 前端 API delete / batchDelete
- [x] 前端表格多选列
- [x] 前端单个删除按钮 + 逻辑
- [x] 前端批量删除按钮 + 逻辑
- [x] 运行中记录保护（前后端）
- [x] 删除后用例不受影响验证
- [x] 功能测试全部通过
- [x] 文档已更新

---

## 六、实际实施记录

### 验证结果

| 测试项 | 结果 |
|--------|------|
| 单个删除（DELETE /{id}） | ✅ 204，记录删除 |
| 批量删除（POST /batch-delete） | ✅ success_count 正确 |
| 用例完好性 | ✅ 25 个用例删除前后不变 |
| 前端 UI 渲染 | ✅ 多选列 + 删除按钮 + 批量删除按钮 |
| 批量删除交互 | ✅ 勾选 2 条 → 弹窗 → 删除 7→5 条 |
| 运行中保护 | ✅ running 记录不可勾选、无删除按钮 |
| 确认弹窗提示 | ✅ 明确"不影响测试用例" |

### 改动文件清单

**后端**:
- `backend/app/services/execution_service.py` - 新增 delete_execution / delete_executions
- `backend/app/routers/executions.py` - 新增 DELETE /{id} 和 POST /batch-delete 路由

**前端**:
- `frontend/src/api/executions.ts` - 新增 delete / batchDelete 方法
- `frontend/src/views/Execution.vue` - 多选列 + 删除按钮 + 批量删除按钮 + 相关方法

**核心约束达成**：删除执行记录只删除执行结果（含级联的 CaseResult），测试用例（TestCase）25 个完全保留，可再次执行生成新记录。

---

## 五、相关文件

| 文件 | 路径 | 用途 |
|------|------|------|
| 执行服务 | `backend/app/services/execution_service.py` | 删除逻辑 |
| 执行路由 | `backend/app/routers/executions.py` | API 接口 |
| 前端 API | `frontend/src/api/executions.ts` | 调用封装 |
| 执行页面 | `frontend/src/views/Execution.vue` | UI 交互 |
| 用例页面 | `frontend/src/views/Cases.vue` | 删除交互参考 |

---

*本文档由 Claude Code 生成，遵循项目执行计划文档规范。*
