# 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 数据展示正常