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

554 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 数据展示正常