# Phase 6: 测试与打磨（W7-W8）

> **目标：** 完善集成测试、E2E 测试、修复 Bug、UI 打磨、编写部署文档，准备 Demo 演示。  
> **周期：** 第 7 ~ 8 周  
> **任务数：** 4 个  
> **可并行：** Task 6.1 / 6.2 / 6.3 可并行  
> **前置依赖：** Phase 5

---

## 本阶段交付物

| 交付物 | 说明 |
|---|---|
| 后端集成测试 | 完整报销流程的自动化测试 |
| 前端 E2E 测试 | Playwright 自动化测试（可选） |
| Bug 修复 + UI 打磨 | 视觉和交互优化 |
| 部署文档 | README + 部署指南 + API 文档 |

---

## 任务清单

### Task 6.1: 后端集成测试

**负责人：** 后端工程师 A  
**预计工时：** 3 天  
**前置依赖：** Phase 5

**Files:**
- Create: `backend/tests/test_integration_flow.py`
- Create: `backend/tests/helpers.py`（测试辅助函数）
- Modify: `backend/tests/conftest.py`（添加测试数据库 fixture）

- [ ] **Step 1: 更新 conftest.py 添加测试数据库 fixture**

`backend/tests/conftest.py`:

```python
import pytest
import asyncio
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.models.base import Base
from app.main import app
from app.core.database import get_db

# 测试数据库 URL
TEST_DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/x_financial_test"

test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
test_session = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)

@pytest.fixture(scope="session")
def event_loop():
    loop = asyncio.new_event_loop()
    yield loop
    loop.close()

@pytest.fixture(autouse=True)
async def setup_database():
    """每个测试前创建表，测试后清理"""
    async with test_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield
    async with test_engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)

@pytest.fixture
async def db():
    async with test_session() as session:
        yield session

@pytest.fixture
async def client(db):
    async def override_get_db():
        yield db

    app.dependency_overrides[get_db] = override_get_db
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac
    app.dependency_overrides.clear()
```

- [ ] **Step 2: 编写测试辅助函数**

`backend/tests/helpers.py`:

```python
from httpx import AsyncClient

async def create_task(client: AsyncClient, user_id="U001", company_id="C001", intent="报北京出差费用") -> dict:
    resp = await client.post("/api/v1/reimbursement/tasks", json={
        "user_id": user_id, "company_id": company_id, "user_intent": intent
    })
    assert resp.status_code == 201
    return resp.json()

async def upload_document(client: AsyncClient, task_id: str, document_type: str, filename: str = "test.pdf") -> dict:
    files = {"file": (filename, b"fake file content", "application/pdf")}
    resp = await client.post(
        f"/api/v1/reimbursement/tasks/{task_id}/documents",
        files=files,
        data={"document_type": document_type}
    )
    assert resp.status_code == 201
    return resp.json()

async def run_agent(client: AsyncClient, task_id: str, start_from="intake") -> dict:
    resp = await client.post(
        f"/api/v1/reimbursement/tasks/{task_id}/agent/run",
        json={"start_from": start_from, "mode": "precheck"}
    )
    assert resp.status_code == 200
    return resp.json()

async def get_draft(client: AsyncClient, task_id: str) -> dict:
    resp = await client.get(f"/api/v1/reimbursement/tasks/{task_id}/draft")
    assert resp.status_code == 200
    return resp.json()

async def get_precheck_result(client: AsyncClient, task_id: str) -> dict:
    resp = await client.get(f"/api/v1/reimbursement/tasks/{task_id}/precheck-result")
    assert resp.status_code == 200
    return resp.json()

async def respond_supplement(client: AsyncClient, task_id: str, supplement_id: str, text: str) -> dict:
    resp = await client.post(
        f"/api/v1/reimbursement/tasks/{task_id}/supplements",
        json={"supplement_request_id": supplement_id, "response_text": text}
    )
    assert resp.status_code == 200
    return resp.json()

async def submit_task(client: AsyncClient, task_id: str) -> dict:
    resp = await client.post(
        f"/api/v1/reimbursement/tasks/{task_id}/submit",
        json={"confirmed": True, "submit_to": "expense_system"}
    )
    assert resp.status_code == 200
    return resp.json()

async def get_sync_status(client: AsyncClient, task_id: str) -> dict:
    resp = await client.get(f"/api/v1/reimbursement/tasks/{task_id}/sync-status")
    assert resp.status_code == 200
    return resp.json()
```

- [ ] **Step 3: 编写完整流程集成测试**

`backend/tests/test_integration_flow.py`:

```python
import pytest
from tests.helpers import *

@pytest.mark.asyncio
async def test_full_reimbursement_flow(client):
    """完整报销流程：创建→上传→识别→草稿→预审→补件→提交→同步"""

    # 1. 创建任务
    task = await create_task(client, intent="我要报这次北京出差的费用")
    task_id = task["task_id"]
    assert task["status"] == "material_collecting"

    # 2. 上传票据
    doc1 = await upload_document(client, task_id, "vat_invoice", "invoice.pdf")
    assert doc1["ocr_status"] == "pending"

    doc2 = await upload_document(client, task_id, "train_ticket", "train.pdf")
    assert doc2["ocr_status"] == "pending"

    doc3 = await upload_document(client, task_id, "hotel_bill", "hotel.pdf")

    # 3. 启动 Agent（使用 mock OCR）
    result = await run_agent(client, task_id, start_from="intake")
    assert result["status"] in ["draft_generated", "prechecking", "need_supplement", "pending_user_confirm"]

    # 4. 获取草稿
    draft = await get_draft(client, task_id)
    assert draft["reimbursement_id"] is not None
    assert len(draft["items"]) > 0
    assert draft["total_amount"] > 0

    # 5. 获取预审结果
    precheck = await get_precheck_result(client, task_id)
    assert "risk_level" in precheck
    assert "precheck_status" in precheck
    assert "rule_hits" in precheck

    # 6. 如果需要补件
    if precheck["precheck_status"] == "need_supplement":
        # 找到需要补件的规则
        for hit in precheck["rule_hits"]:
            if hit["action"] == "require_attachment":
                # 补充附件
                await upload_document(client, task_id, "hotel_bill", "hotel_supplement.pdf")
                await respond_supplement(client, task_id, hit.get("id", "S001"), "已补充酒店流水")

        # 重新预审
        await run_agent(client, task_id, start_from="precheck")
        precheck2 = await get_precheck_result(client, task_id)

    # 7. 确认提交
    submit = await submit_task(client, task_id)
    assert submit["status"] == "submitting"

    # 8. 检查同步状态
    sync = await get_sync_status(client, task_id)
    assert sync["sync_status"] in ["success", "pending"]

@pytest.mark.asyncio
async def test_create_task_without_intent(client):
    """测试不提供意图时创建任务"""
    resp = await client.post("/api/v1/reimbursement/tasks", json={
        "user_id": "U001", "company_id": "C001"
    })
    assert resp.status_code == 422  # Validation error

@pytest.mark.asyncio
async def test_get_nonexistent_task(client):
    """测试查询不存在的任务"""
    resp = await client.get("/api/v1/reimbursement/tasks/nonexistent-id")
    assert resp.status_code == 404

@pytest.mark.asyncio
async def test_list_tasks_pagination(client):
    """测试任务列表分页"""
    # 创建多个任务
    for i in range(5):
        await create_task(client, intent=f"test task {i}")

    # 测试分页
    resp = await client.get("/api/v1/reimbursement/tasks?page=1&size=3")
    assert resp.status_code == 200
    data = resp.json()
    assert data["total"] >= 5
    assert len(data["items"]) <= 3
```

- [ ] **Step 4: 编写规则引擎集成测试**

测试每条规则对真实报销数据的命中情况：
- 住宿费超标 → 命中 `TRAVEL_HOTEL_LIMIT`
- 缺少酒店流水 → 命中 `HOTEL_BILL_REQUIRED`
- 重复发票 → 命中 `DUPLICATE_INVOICE_CHECK`
- 合规报销 → 无命中

- [ ] **Step 5: 确保所有测试通过**

Run: `cd backend && pytest tests/ -v --tb=short`
Expected: All PASS

- [ ] **Step 6: Commit**

```bash
git add backend/
git commit -m "test: 添加完整报销流程集成测试"
```

---

### Task 6.2: 前端 E2E 测试（可选）

**负责人：** 前端工程师  
**预计工时：** 2 天  
**前置依赖：** Phase 5  
**可并行于：** Task 6.1、6.3

**Files:**
- Create: `frontend/e2e/reimbursement.spec.ts`
- Create: `frontend/playwright.config.ts`

- [ ] **Step 1: 安装 Playwright**

```bash
cd frontend
npm install -D @playwright/test
npx playwright install chromium
```

- [ ] **Step 2: 配置 Playwright**

`frontend/playwright.config.ts`:

```typescript
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  baseURL: 'http://localhost:5173',
  use: {
    headless: true,
    screenshot: 'only-on-failure',
    trace: 'retain-on-failure',
  },
  webServer: {
    command: 'npm run dev',
    port: 5173,
    reuseExistingServer: true,
  },
})
```

- [ ] **Step 3: 编写核心流程 E2E 测试**

`frontend/e2e/reimbursement.spec.ts`:

```typescript
import { test, expect } from '@playwright/test'

test('完整报销流程', async ({ page }) => {
  // 1. 访问首页
  await page.goto('/')
  await expect(page.locator('h1')).toContainText('报销')

  // 2. 输入报销意图
  await page.fill('input[placeholder*="报销"]', '我要报这次北京出差的费用')
  await page.click('button:has-text("提交")')

  // 3. 跳转到上传页
  await expect(page).toHaveURL(/\/task\/.*\/upload/)

  // 4. 上传文件
  await page.setInputFiles('input[type="file"]', 'tests/fixtures/invoice.pdf')

  // 5. 选择票据类型
  await page.selectOption('select', 'vat_invoice')

  // 6. 开始识别
  await page.click('button:has-text("开始识别")')

  // 7. 跳转到草稿页
  await expect(page).toHaveURL(/\/draft/, { timeout: 30000 })

  // 8. 执行预审
  await page.click('button:has-text("执行预审")')

  // 9. 跳转到预审结果页
  await expect(page).toHaveURL(/\/precheck/, { timeout: 30000 })
})
```

- [ ] **Step 4: 运行 E2E 测试**

Run: `cd frontend && npx playwright test`
Expected: All PASS

- [ ] **Step 5: Commit**

```bash
git add frontend/
git commit -m "test: 添加前端 E2E 测试"
```

---

### Task 6.3: Bug 修复与 UI 打磨

**负责人：** 全员参与  
**预计工时：** 3 天  
**前置依赖：** Phase 5  
**可并行于：** Task 6.1、6.2

- [ ] **Step 1: UI 走查清单**

逐页面检查：

| 页面 | 检查项 |
|---|---|
| 首页 | 布局、输入框交互、快捷按钮、最近任务列表 |
| 上传页 | 拖拽上传、文件预览、票据类型选择、进度条 |
| 草稿页 | 表格编辑、金额汇总、附件预览、预审按钮 |
| 预审结果页 | 结论卡片、风险项展示、规则命中详情 |
| 补件页 | 补件清单、上传/回复交互、提交反馈 |
| 确认页 | 摘要展示、同步状态轮询、成功/失败状态 |
| 审计日志页 | 时间线展示、筛选功能 |

- [ ] **Step 2: 修复共性问题**

- [ ] 响应式布局适配（1280px / 1024px / 768px 断点）
- [ ] Loading 状态：所有异步操作加 loading 指示器
- [ ] 错误提示：API 错误统一使用 Ant Design Message 提示
- [ ] 空状态：无数据时展示空状态插画和文案
- [ ] 表单校验：必填项红框提示 + 校验文案
- [ ] 金额格式化：千分位 + 两位小数 + ¥ 前缀
- [ ] 日期格式化：YYYY-MM-DD
- [ ] 确认弹窗：删除、提交等危险操作二次确认

- [ ] **Step 3: 添加 Demo 展示数据**

在首页添加"体验 Demo"按钮，一键生成演示数据：
- 创建一个已完成全流程的报销任务
- 包含 3 条费用明细
- 有规则命中记录
- 有审计日志

- [ ] **Step 4: 性能优化**

- [ ] 路由懒加载（已配置）
- [ ] 表格虚拟滚动（如果明细很多）
- [ ] 图片懒加载
- [ ] API 请求去重/缓存

- [ ] **Step 5: Commit**

```bash
git add .
git commit -m "fix: UI 打磨和 Bug 修复"
```

---

### Task 6.4: 部署与文档

**负责人：** 后端工程师 B + 前端工程师  
**预计工时：** 2 天  
**前置依赖：** Task 6.3  

**Files:**
- Create: `docker-compose.prod.yml`
- Create: `nginx.conf`
- Modify: `README.md`
- Create: `docs/deployment.md`

- [ ] **Step 1: 编写生产 Docker Compose**

`docker-compose.prod.yml`:

```yaml
version: "3.8"
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./frontend/dist:/usr/share/nginx/html
    depends_on:
      - backend

  backend:
    build: ./backend
    environment:
      - DATABASE_URL=postgresql+asyncpg://postgres:${DB_PASSWORD}@postgres:5432/x_financial
      - REDIS_URL=redis://redis:6379/0
      - MINIO_ENDPOINT=minio:9000
    depends_on:
      - postgres
      - redis
      - minio

  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: x_financial
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

  minio:
    image: minio/minio
    command: server /data
    environment:
      MINIO_ROOT_USER: ${MINIO_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD}
    volumes:
      - minio_data:/data

volumes:
  pgdata:
  minio_data:
```

- [ ] **Step 2: 编写 Nginx 配置**

`nginx.conf`:

```nginx
server {
    listen 80;
    server_name localhost;

    # 前端静态文件
    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }

    # 后端 API 代理
    location /api/ {
        proxy_pass http://backend:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 120s;
    }
}
```

- [ ] **Step 3: 编写部署文档**

`docs/deployment.md` — 包含：
- 环境要求（Docker、Docker Compose）
- 配置说明（.env 文件）
- 启动步骤
- 停止和重启
- 数据库迁移
- 种子数据初始化
- 日志查看
- 常见问题排查

- [ ] **Step 4: 更新 README**

项目 README 包含：
- 项目简介和架构图
- 快速启动（开发环境）
- 技术栈说明
- 目录结构
- 开发指南
- API 文档链接

- [ ] **Step 5: 确认 Swagger 文档完整**

访问 http://localhost:8000/docs，确认：
- 所有 API 端点都有描述
- 请求/响应示例完整
- 错误码说明完整

- [ ] **Step 6: Commit**

```bash
git add .
git commit -m "docs: 添加部署文档、Nginx 配置、生产 Docker Compose"
```

---

## 本阶段完成检查

- [ ] `cd backend && pytest tests/ -v` 全部通过
- [ ] `cd frontend && npx playwright test` 全部通过（如配置）
- [ ] `cd frontend && npm run build` 无报错
- [ ] 完整报销流程在浏览器中手动测试无问题
- [ ] 所有页面响应式布局正常
- [ ] `docker-compose -f docker-compose.prod.yml up -d` 能启动
- [ ] README 和部署文档完整
- [ ] Swagger API 文档完整
- [ ] Demo 数据展示正常
