# 功能测试用例 Excel 格式适配 · 计划执行文档

> **文档状态**：已定稿 · **版本**：v1.0 · **最后更新**：2026-08-17  
> **维护者**：czj · **所属模块**：报告中心-功能测试  
> **对应 PRD**：`_PRD_功能测试用例Excel适配.md`

---

## 一、执行计划总览

| 阶段 | 内容 | 预估工时 | 产出物 |
|------|------|----------|--------|
| Phase 1 | 后端：excel_reader.py 多 Sheet 遍历改造 | 15min | `excel_reader.py` 修改 |
| Phase 2 | 后端：config.py 新增测试结果映射 | 5min | `config.py` 修改 |
| Phase 3 | 验证：用新格式 Excel 文件测试 | 10min | 控制台验证输出 |

**总预估工时：30 分钟**

---

## 二、Phase 1：excel_reader.py 多 Sheet 遍历改造

### 修改文件：`backend/app/services/functional/excel_reader.py`

**改动点**：`read_test_cases()` 函数中，将 `ws = wb.active` 改为遍历 `wb.sheetnames` 所有 Sheet。

### 当前代码（第 164-165 行）

```python
def read_test_cases(file_path: str) -> List[TestCase]:
    wb = openpyxl.load_workbook(file_path, data_only=True)
    ws = wb.active
```

### 目标代码

```python
def read_test_cases(file_path: str) -> List[TestCase]:
    wb = openpyxl.load_workbook(file_path, data_only=True)
    test_cases = []

    for sheet_name in wb.sheetnames:          # ← 遍历所有 Sheet
        ws = wb[sheet_name]
        for row in ws.iter_rows(min_row=CASE_DATA_START_ROW):
            serial_number = _get_int_cell_value(row, CASE_COL_SERIAL_NUMBER)

            if serial_number == 0:
                continue

            case = TestCase(
                serial_number=serial_number,
                # ... 其余字段不变 ...
            )
            test_cases.append(case)

    wb.close()
    return test_cases
```

### 不变的部分

- `CASE_DATA_START_ROW` 常量（仍为 4）
- 所有列索引常量（`CASE_COL_*`）
- 数据行的`serial_number == 0` 跳过逻辑
- `_get_cell_value()` / `_get_int_cell_value()` 工具函数
- `read_bug_list()` 函数（BUG 列表仍为单 Sheet，无需修改）

---

## 三、Phase 2：excel_reader.py 新增测试结果映射

### 修改文件：`backend/app/services/functional/excel_reader.py`

> 注：`normalize_test_result()` 及其 `pass_aliases` 定义位于 `excel_reader.py`（非 config.py），已按实际位置修改。

**改动点**：在 `pass_aliases` 集合中新增 `"功能验证"`。

### 当前代码（第 308 行）

```python
pass_aliases = {"通过", "Pass", "PASS", "pass", "成功", "P", "√", "✓", "是", "yes", "Yes", "YES"}
```

### 目标代码

```python
pass_aliases = {"通过", "Pass", "PASS", "pass", "成功", "P", "√", "✓", "是", "yes", "Yes", "YES", "功能验证"}
```

---

## 四、Phase 3：验证

### 验证脚本

```python
from app.services.functional.excel_reader import read_test_cases, normalize_test_result

# 验证 1：多 Sheet 读取
cases = read_test_cases("临时目录/大亚湾一体化会议系统测试用例-260811.xlsx")
assert len(cases) == 277, f"预期 277 条，实际 {len(cases)}"

# 验证 2：测试结果统计
from collections import Counter
result_counter = Counter(c.test_result for c in cases)
print("测试结果分布:", dict(result_counter))

# 验证 3：模块分布
modules = set(c.module for c in cases)
print(f"模块数: {len(modules)}")
print("模块列表:", sorted(modules))

# 验证 4：旧格式兼容
old_cases = read_test_cases("data/input/用例模板.xlsx")
assert len(old_cases) > 0, "旧格式读取失败"
```

### 人工验证点

| 验证项 | 检查方法 | 预期 |
|--------|---------|------|
| 总用例数 | 统计 len(cases) | 277 |
| "功能验证"映射 | 筛选 `test_result="通过"` 的用例 | 包含原"功能验证"的用例 |
| 模块分组 | 统计 module 字段 | 覆盖 3 个 Sheet 的模块 |
| 旧格式回归 | 读取原模板文件 | 数据量不变 |

---

## 五、潜在风险与回退方案

| 风险 | 概率 | 应对 |
|------|------|------|
| 某 Sheet 有额外的合并单元格导致读取异常 | 低 | openpyxl 对合并单元格会返回 None，`_get_cell_value()` 已处理 |
| 某 Sheet 列顺序不一致 | 低 | 检查确认 3 个 Sheet 列结构完全一致 |
| 旧格式文件有不可见 Sheet | 低 | 不影响，旧文件只有 1 个 Sheet |

回退方案：`git checkout -- backend/app/services/functional/excel_reader.py` 恢复单 Sheet 读取。

---

*本文档由 Claude Code 生成，供 prd-code 流程使用。*