Files
X-Financial/docs/plans/phase-6-testing-polish/README.md
WIN-JHFT4D3SIVT\caoxiaozhu 7141e1d11a feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00

15 KiB
Raw Blame History

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:

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:

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:

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
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

cd frontend
npm install -D @playwright/test
npx playwright install chromium
  • Step 2: 配置 Playwright

frontend/playwright.config.ts:

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:

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
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

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:

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:

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

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