# 功能测试报告-模板配置 · 计划执行文档

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

---

## 一、执行计划总览

| 阶段 | 内容 | 预估工时 | 产出物 |
|------|------|----------|--------|
| Phase 1 | 后端：模板模型 + 配置工具函数 | 1h | `report_template.py` / `__init__.py` / `config.py` 改造 |
| Phase 2 | 后端：生成器改造（word / chart / markdown） | 2h | 三个生成器支持 `template_config` |
| Phase 3 | 后端：服务层 + API 路由 + 注册 | 1h | `report_template.py` 路由 / `functional_report.py` 扩展 / `main.py` 注册 |
| Phase 4 | 前端：模板配置页面 + API 封装 | 2h | `TemplateConfig.vue` / `reportTemplate.ts` |
| Phase 5 | 前端：路由 + 菜单 + 生成页模板下拉框 | 1h | `router/index.ts` / `App.vue` / `FunctionalReport.vue` |
| Phase 6 | 验证与部署 | 1h | 端到端验证 + 部署到 5.60 |

---

## 二、Phase 1：后端模型与配置工具

### 1.1 新建 `backend/app/models/report_template.py`

```python
class ReportTemplate(Base):
    __tablename__ = "report_templates"

    id: int = Column(Integer, primary_key=True, autoincrement=True, comment="主键")
    name: str = Column(String(100), nullable=False, comment="模板名称")
    is_default: bool = Column(Boolean, nullable=False, default=False, comment="是否默认模板")
    config: str = Column(Text, nullable=False, comment="完整配置 JSON 字符串")
    created_at: datetime = Column(DateTime, default=datetime.now, comment="创建时间")
    updated_at: datetime = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")

    def to_dict(self):
        """转为字典，config 反序列化为 dict"""
        return {
            "id": self.id,
            "name": self.name,
            "is_default": self.is_default,
            "config": json.loads(self.config) if self.config else {},
            "created_at": self.created_at.strftime(DATE_FORMAT) if self.created_at else None,
            "updated_at": self.updated_at.strftime(DATE_FORMAT) if self.updated_at else None,
        }

    def to_list_item(self):
        """列表项，不含 config 详情"""
        return {
            "id": self.id,
            "name": self.name,
            "is_default": self.is_default,
            "updated_at": self.updated_at.strftime(DATE_FORMAT) if self.updated_at else None,
        }
```

### 1.2 修改 `backend/app/models/__init__.py`

- 第 25 行附近新增 `from app.models.report_template import ReportTemplate`
- `__all__` 中新增 `"ReportTemplate"`

### 1.3 修改 `backend/app/services/functional/config.py`

新增两个工具函数（保留原有常量不变，确保向后兼容）：

```python
def get_default_config() -> dict:
    """返回当前硬编码默认值作为默认模板配置"""
    return {
        "chapters": {
            "report_basic_info": True, "execution_summary": True,
            "test_scope": True, "case_details": True, "bug_list": True,
            "chart_analysis": True, "execution_records": True,
            "attachments": True, "risk_assessment": True, "appendix": True,
        },
        "defaults": {
            "test_lead": DEFAULT_TEST_LEAD, "tester": DEFAULT_TESTER,
            "test_env": DEFAULT_TEST_ENV, "system_version": DEFAULT_SYSTEM_VERSION,
            "server_address": DEFAULT_SERVER_ADDRESS, "deployment_env": DEFAULT_DEPLOYMENT_ENV,
        },
        "word_style": {
            "font_name": WORD_FONT_NAME, "font_size": WORD_FONT_SIZE,
            "title_level_1_size": WORD_TITLE_LEVEL_1_SIZE,
            "title_level_2_size": WORD_TITLE_LEVEL_2_SIZE,
            "title_level_3_size": WORD_TITLE_LEVEL_3_SIZE,
            "title_level_4_size": WORD_TITLE_LEVEL_4_SIZE,
            "paragraph_first_line_indent": WORD_PARAGRAPH_FIRST_LINE_INDENT,
            "title_color": "#000000",
        },
        "chart_style": {
            "dpi": CHART_DPI, "figure_width": CHART_FIGURE_SIZE[0],
            "figure_height": CHART_FIGURE_SIZE[1],
            "chinese_font": CHINESE_FONT,
            "colors": dict(CHART_COLORS),
        },
    }

def merge_template_config(base_config: dict, template_config: dict) -> dict:
    """将模板配置与默认配置深合并，缺失字段用默认值填充"""
    def deep_merge(base, override):
        result = dict(base)
        for k, v in (override or {}).items():
            if isinstance(v, dict) and isinstance(result.get(k), dict):
                result[k] = deep_merge(result[k], v)
            else:
                result[k] = v
        return result

    return deep_merge(base_config, template_config or {})
```

---

## 三、Phase 2：生成器改造

### 2.1 修改 `backend/app/services/functional/word_generator.py`

`generate_report()` 新增可选参数 `template_config: Optional[Dict] = None`：

1. **加载样式**：函数开头 `cfg = merge_template_config(get_default_config(), template_config)`，从中读取：
   - `cfg["word_style"]["font_name"]` / `font_size` / `title_level_*_size` / `title_color` / `paragraph_first_line_indent`
   - `cfg["defaults"]["test_lead"]` / `tester` 等（替代硬编码 `DEFAULT_*` 常量）
2. **章节开关**：`chapters = cfg["chapters"]`，每个章节生成逻辑前加：
   ```python
   if not chapters.get("report_basic_info", True):
       print("[word] 跳过章节: 报告基本信息")
   else:
       # 原有章节生成逻辑
   ```
3. **动态编号**：章节标题序号不硬编码，维护一个 `chapter_index` 计数器，仅对启用的章节自增
4. **标题样式**：`_add_title(doc, title, level)` 函数从 `cfg["word_style"]` 读取字号映射和标题颜色

### 2.2 修改 `backend/app/services/functional/chart_generator.py`

`generate_all_charts()` 新增可选参数 `template_config: Optional[Dict] = None`：

- 从 `template_config["chart_style"]` 读取 `dpi`、`figure_width`、`figure_height`、`chinese_font`、`colors`
- `_setup_chinese_font()` 优先使用配置的中文字体，失败时回退到候选列表
- 各图表函数的 `plt.rcParams` 设置（dpi、字体）动态传入

### 2.3 修改 `backend/app/services/functional/markdown_generator.py`

与 word_generator 类似，`generate_markdown_report()` 新增 `template_config: Optional[Dict] = None`：

- 章节开关：跳过被关闭的章节
- 默认值：从 `cfg["defaults"]` 读取

---

## 四、Phase 3：服务层与 API 路由

### 3.1 新建 `backend/app/schemas/report_template.py`

```python
class ReportTemplateResponse(BaseModel):
    id: int
    name: str
    is_default: bool
    config: dict = {}
    created_at: Optional[str] = None
    updated_at: Optional[str] = None

class ReportTemplateListItem(BaseModel):
    id: int
    name: str
    is_default: bool
    updated_at: Optional[str] = None

class ReportTemplateCreateRequest(BaseModel):
    name: str = Field(..., min_length=1, max_length=100, description="模板名称")
    config: dict = Field(default_factory=dict, description="完整配置 JSON")

class ReportTemplateUpdateRequest(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    config: Optional[dict] = None
    is_default: Optional[bool] = None
```

### 3.2 新建 `backend/app/routers/report_template.py`

注册到 `backend/app/main.py`，前缀 `/api/functional-report/templates`：

| 方法 | 端点 | 实现要点 |
|------|------|----------|
| GET | `/` | 查询全部模板，按 is_default desc + id asc 排序，返回 ListItem 列表 |
| GET | `/{template_id}` | 查询单个模板，404 兜底 |
| POST | `/` | 创建模板；若配置为空则用 `get_default_config()` 填充；若库中无模板则自动设为默认 |
| PUT | `/{template_id}` | 更新 name / config / is_default；若置为新默认则先批量取消其他默认 |
| DELETE | `/{template_id}` | 校验非最后一条，否则 400 拒绝 |
| PUT | `/{template_id}/default` | 事务内先 `UPDATE ... SET is_default=False`，再设置目标为 True |

### 3.3 修改 `backend/app/services/functional_report_service.py`

- `analyze_and_generate()` 与 `generate_word_report()` / `generate_markdown_report()` 新增 `template_id: Optional[int] = None` 参数
- 生成时：若 `template_id` 提供 → 加载该模板 config；否则查询默认模板（`is_default=True`）；均无 → `template_config=None`（生成器内部回退硬编码）
- 使用 `get_db()` 会话加载（与 ERP 配置模式一致）

### 3.4 修改 `backend/app/routers/functional_report.py` + `schemas/functional_report.py`

- `GenerateRequest` 新增 `template_id: Optional[int] = Field(None, description="报告模板ID")`
- generate 端点将 `template_id` 传入 service

### 3.5 修改 `backend/app/main.py`

```python
from app.routers import report_template
app.include_router(report_template.router, prefix="/api/functional-report", tags=["功能测试报告-模板配置"])
```

---

## 五、Phase 4：前端页面与 API

### 4.1 新建 `frontend/src/api/reportTemplate.ts`

```typescript
export interface ReportTemplateListItem { id: number; name: string; is_default: boolean; updated_at?: string }
export interface ReportTemplateConfig { chapters: Record<string, boolean>; defaults: Record<string, string>; word_style: {...}; chart_style: {...} }
export interface ReportTemplate extends ReportTemplateListItem { config: ReportTemplateConfig; created_at?: string }

export const reportTemplateApi = {
  list(): request.get('/api/functional-report/templates'),
  get(id: number): request.get(`/api/functional-report/templates/${id}`),
  create(data: {name: string; config: object}): request.post('/api/functional-report/templates', data),
  update(id: number, data: {name?: string; config?: object; is_default?: boolean}): request.put(`/api/functional-report/templates/${id}`, data),
  remove(id: number): request.delete(`/api/functional-report/templates/${id}`),
  setDefault(id: number): request.put(`/api/functional-report/templates/${id}/default`)
}
```

### 4.2 新建 `frontend/src/views/functional-report/TemplateConfig.vue`

页面布局：

```
┌────────────────────────────────────────────┐
│ [el-row gutter=16]                         │
│  ┌────────────┬─────────────────────────┐  │
│  │ 左列 320px  │ 右列 flex-1            │  │
│  │ [el-card]   │ [el-card]              │  │
│  │  模板列表    │  配置表单               │  │
│  │  [+ 新建]   │  1) 基本信息            │  │
│  │  模板行     │  2) 章节开关 × 10       │  │
│  │  [默认][编辑]│  3) Word 样式          │  │
│  │  [删除]     │  4) 图表样式            │  │
│  │            │  5) 报告默认值           │  │
│  │            │  [保存]                 │  │
│  └────────────┴─────────────────────────┘  │
└────────────────────────────────────────────┘
```

- 左侧 `el-card`：模板列表（`el-table`），行内操作按钮：设为默认（el-button link）/ 编辑（切换选中）/ 删除（`ElMessageBox.confirm`）
- 顶部"新建模板"按钮 → 右侧表单重置为默认配置（调 `GET /config` 或前端硬编码默认结构）
- 右侧 `el-form`，分 5 个区块（`el-divider` 分隔），控件类型见 PRD §2.2：
  - 章节开关：`el-switch` × 10，中英文标签并排
  - Word 样式：`el-input`（字体）+ `el-input-number`（字号/缩进）+ `el-color-picker`（标题颜色）
  - 图表样式：`el-input-number`（DPI/尺寸）+ `el-input`（字体）+ `el-color-picker` × 8
  - 报告默认值：`el-input` × 6
- 保存：调 `create` 或 `update`，成功后刷新列表并 `ElMessage.success`
- 加载中 `v-loading`，无模板时右侧显示空状态提示

---

## 六、Phase 5：前端路由、菜单与生成页接入

### 5.1 修改 `frontend/src/router/index.ts`

在 `/reports/functional` children 中新增：

```typescript
{
  path: 'template-config',
  name: 'TemplateConfig',
  component: () => import('@/views/functional-report/TemplateConfig.vue'),
  meta: { title: '模板配置' }
}
```

### 5.2 修改 `frontend/src/App.vue`

- 功能父菜单下新增子菜单项：`<el-menu-item index="/reports/functional/template-config">模板配置</el-menu-item>`（放在"生成报告"之后、"ERP 配置"之前或之后均可）
- `FUNCTIONAL_LABELS` 新增：`'template-config': '模板配置'`

### 5.3 修改 `frontend/src/views/functional-report/FunctionalReport.vue`

- 步骤2项目配置中，在"报告格式"下拉框旁新增"报告模板" `el-select`
- `onMounted` 时 `reportTemplateApi.list()` 加载模板，默认选中 `is_default` 的模板
- 生成时在请求参数中携带 `template_id`

---

## 七、验证方案

### 7.1 后端验证

| 测试项 | 步骤 | 预期 |
|--------|------|------|
| 建表 | 重启后端 | 自动创建 `report_templates` 表 |
| 创建模板 | POST /templates | 返回新模板，第一条自动为默认 |
| 列表 | GET /templates | 返回列表含 is_default 标记 |
| 设为默认 | PUT /templates/{id}/default | 原默认取消，新默认生效 |
| 删除保护 | 删除唯一模板 | 返回 400 且不删除 |
| 生成报告（硬件码回退） | POST /generate（无 template_id） | 报告正常生成，样式与之前一致 |
| 生成报告（带模板） | 创建关闭附件的模板 → generate 带 template_id | Word 报告中无"测试附件"章节 |
| 生成报告（样式） | 修改模板字号/颜色 → generate | Word 中字号/颜色与配置一致 |

### 7.2 前端验证

| 测试项 | 步骤 | 预期 |
|--------|------|------|
| 页面加载 | 访问 /reports/functional/template-config | 左侧模板列表 + 右侧配置表单正常渲染 |
| 新建模板 | 点击"新建模板" | 右侧表单重置，可保存 |
| 编辑模板 | 选中模板修改配置并保存 | 列表刷新，数据持久化 |
| 章节开关 | 关闭某章节并保存 | 列表/详情中该开关为关闭态 |
| 生成页选择 | 访问 /reports/functional/generate | 模板下拉框出现，默认选中默认模板 |

### 7.3 部署验证

1. `cd frontend && npm run build` 构建前端
2. 使用 `_upload_frontend_fix.py` 上传 dist 并验证 HTTP 状态
3. 后端代码 `scp` 到 5.60，`docker compose restart app`
4. 访问 http://192.168.5.60/reports/functional/template-config 端到端验证

---

## 八、文件变更清单

### 后端新增

| 文件 | 说明 |
|------|------|
| `backend/app/models/report_template.py` | 模板 ORM 模型 |
| `backend/app/schemas/report_template.py` | 模板 Pydantic 校验 |
| `backend/app/routers/report_template.py` | 模板 CRUD API 路由 |

### 后端修改

| 文件 | 修改内容 |
|------|---------|
| `backend/app/models/__init__.py` | 导入 ReportTemplate |
| `backend/app/services/functional/config.py` | 新增 get_default_config() / merge_template_config() |
| `backend/app/services/functional/word_generator.py` | 支持 template_config（章节开关+样式+默认值） |
| `backend/app/services/functional/chart_generator.py` | 支持 chart_style 配置 |
| `backend/app/services/functional/markdown_generator.py` | 支持 template_config（章节开关+默认值） |
| `backend/app/services/functional_report_service.py` | 加载模板配置并传递给生成器 |
| `backend/app/routers/functional_report.py` | generate 端点传入 template_id |
| `backend/app/schemas/functional_report.py` | GenerateRequest 新增 template_id |
| `backend/app/main.py` | 注册 report_template 路由 |

### 前端新增

| 文件 | 说明 |
|------|------|
| `frontend/src/api/reportTemplate.ts` | 模板 CRUD API 封装 |
| `frontend/src/views/functional-report/TemplateConfig.vue` | 模板配置管理页面 |

### 前端修改

| 文件 | 修改内容 |
|------|---------|
| `frontend/src/router/index.ts` | 新增 template-config 子路由 |
| `frontend/src/App.vue` | 新增菜单项 + FUNCTIONAL_LABELS |
| `frontend/src/views/functional-report/FunctionalReport.vue` | 新增模板下拉选择 |

---

## 九、实施顺序

```
Phase 1: 模板模型 → 注册 __init__ → config.py 配置工具函数
    ↓
Phase 2: word_generator 改造 → chart_generator 改造 → markdown_generator 改造
    ↓
Phase 3: schemas → report_template 路由 → 服务层改造 → generate 扩展 → main.py 注册
    ↓
Phase 4: reportTemplate.ts → TemplateConfig.vue
    ↓
Phase 5: router → App.vue 菜单 → FunctionalReport.vue 模板下拉
    ↓
Phase 6: 后端启动验证 → 前端构建验证 → 部署 5.60 → 端到端验证
```