1603 lines
46 KiB
Markdown
1603 lines
46 KiB
Markdown
|
|
# AI 报销预审中台 MVP 实施计划
|
|||
|
|
|
|||
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|||
|
|
|
|||
|
|
**Goal:** 在 8 周内完成 AI 报销预审中台的 MVP,跑通「上传材料 → OCR 识别 → 草稿生成 → 规则预审 → 补件交互 → 用户确认 → 模拟同步」完整闭环,优先支持差旅报销场景。
|
|||
|
|
|
|||
|
|
**Architecture:** 采用模块化单体架构,前端 Vue3 + Ant Design Vue,后端 Python FastAPI,数据库 PostgreSQL + Redis,文件存储 MinIO,OCR 先用百度/腾讯云 API 封装。Agent 编排用自研轻量状态机 + LangGraph(可选),规则引擎自研 JSON Rule Engine。6 层架构:用户入口 → AI 操作层 → Agent 层 → 影子账本 → Policy & Evidence → System Adapter。
|
|||
|
|
|
|||
|
|
**Tech Stack:**
|
|||
|
|
- 前端:Vue 3 + TypeScript + Ant Design Vue + Vite + Pinia
|
|||
|
|
- 后端:Python 3.11+ / FastAPI + SQLAlchemy + Alembic + Pydantic v2
|
|||
|
|
- 数据库:PostgreSQL 15 + Redis 7
|
|||
|
|
- 文件存储:MinIO(S3 兼容)
|
|||
|
|
- OCR:百度云 OCR API(增值税发票、火车票、机票行程单)
|
|||
|
|
- 规则引擎:自研 JSON Rule Engine
|
|||
|
|
- Agent:自研 Orchestrator 状态机 + 大模型 API(OpenAI / 国内模型)
|
|||
|
|
- 部署:Docker Compose
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 团队分工建议(3-5 人)
|
|||
|
|
|
|||
|
|
| 角色 | 人数 | 职责 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 后端工程师 A | 1 | 核心后端:任务管理、影子账本、Agent 编排、规则引擎 |
|
|||
|
|
| 后端工程师 B | 1 | OCR 集成、文件服务、适配器层、审计日志 |
|
|||
|
|
| 前端工程师 | 1-2 | 所有页面与组件(可拆分为两人并行) |
|
|||
|
|
| 全栈/Agent 工程师 | 1 | Agent Prompt 设计、大模型集成、规则配置 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 总体里程碑
|
|||
|
|
|
|||
|
|
| 周 | 里程碑 | 核心交付 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| W1 | 项目基建 + 数据模型 | 项目骨架、数据库 schema、开发环境 |
|
|||
|
|
| W2-W3 | 后端核心服务 | 任务/票据/OCR/规则引擎 API |
|
|||
|
|
| W3-W4 | Agent 编排 + 影子账本 | Orchestrator 状态机、5 个 Agent |
|
|||
|
|
| W4-W5 | 前端核心页面 | 入口/上传/草稿/预审/补件/确认 |
|
|||
|
|
| W5-W6 | 前后端联调 + 规则配置 | 完整流程跑通 |
|
|||
|
|
| W7-W8 | 集成测试 + 打磨 + 部署 | E2E 测试、修复、演示 Demo |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 1: 项目基建(W1)
|
|||
|
|
|
|||
|
|
### Task 1.1: 后端项目骨架搭建
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/__init__.py`
|
|||
|
|
- Create: `backend/app/main.py`
|
|||
|
|
- Create: `backend/app/core/config.py`
|
|||
|
|
- Create: `backend/app/core/database.py`
|
|||
|
|
- Create: `backend/app/core/dependencies.py`
|
|||
|
|
- Create: `backend/app/api/__init__.py`
|
|||
|
|
- Create: `backend/app/api/v1/__init__.py`
|
|||
|
|
- Create: `backend/app/api/v1/router.py`
|
|||
|
|
- Create: `backend/app/models/__init__.py`
|
|||
|
|
- Create: `backend/app/schemas/__init__.py`
|
|||
|
|
- Create: `backend/app/services/__init__.py`
|
|||
|
|
- Create: `backend/requirements.txt`
|
|||
|
|
- Create: `backend/pyproject.toml`
|
|||
|
|
- Create: `backend/Dockerfile`
|
|||
|
|
- Create: `backend/alembic.ini`
|
|||
|
|
- Create: `backend/alembic/env.py`
|
|||
|
|
- Test: `backend/tests/__init__.py`
|
|||
|
|
- Test: `backend/tests/conftest.py`
|
|||
|
|
- Test: `backend/tests/test_health.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 初始化后端项目结构**
|
|||
|
|
|
|||
|
|
创建 FastAPI 项目骨架,使用以下目录结构:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
backend/
|
|||
|
|
├── app/
|
|||
|
|
│ ├── __init__.py
|
|||
|
|
│ ├── main.py # FastAPI 应用入口
|
|||
|
|
│ ├── core/
|
|||
|
|
│ │ ├── config.py # Settings(Pydantic BaseSettings)
|
|||
|
|
│ │ ├── database.py # SQLAlchemy async engine + session
|
|||
|
|
│ │ └── dependencies.py # 通用依赖注入(db session, 当前用户等)
|
|||
|
|
│ ├── api/
|
|||
|
|
│ │ └── v1/
|
|||
|
|
│ │ ├── __init__.py
|
|||
|
|
│ │ ├── router.py # v1 路由聚合
|
|||
|
|
│ │ ├── tasks.py # 报销任务 API
|
|||
|
|
│ │ ├── documents.py # 票据附件 API
|
|||
|
|
│ │ ├── precheck.py # 预审结果 API
|
|||
|
|
│ │ └── supplements.py # 补件 API
|
|||
|
|
│ ├── models/ # SQLAlchemy ORM models
|
|||
|
|
│ │ ├── __init__.py
|
|||
|
|
│ │ ├── task.py
|
|||
|
|
│ │ ├── reimbursement.py
|
|||
|
|
│ │ ├── document.py
|
|||
|
|
│ │ ├── rule.py
|
|||
|
|
│ │ └── audit.py
|
|||
|
|
│ ├── schemas/ # Pydantic schemas
|
|||
|
|
│ │ ├── __init__.py
|
|||
|
|
│ │ ├── task.py
|
|||
|
|
│ │ ├── reimbursement.py
|
|||
|
|
│ │ ├── document.py
|
|||
|
|
│ │ └── rule.py
|
|||
|
|
│ ├── services/ # 业务逻辑层
|
|||
|
|
│ │ ├── __init__.py
|
|||
|
|
│ │ ├── task_service.py
|
|||
|
|
│ │ ├── document_service.py
|
|||
|
|
│ │ ├── ocr_service.py
|
|||
|
|
│ │ ├── rule_engine.py
|
|||
|
|
│ │ └── sync_service.py
|
|||
|
|
│ └── agents/ # Agent 编排层
|
|||
|
|
│ ├── __init__.py
|
|||
|
|
│ ├── orchestrator.py
|
|||
|
|
│ ├── intake_agent.py
|
|||
|
|
│ ├── parse_agent.py
|
|||
|
|
│ ├── rule_check_agent.py
|
|||
|
|
│ ├── explain_agent.py
|
|||
|
|
│ └── sync_agent.py
|
|||
|
|
├── alembic/
|
|||
|
|
│ ├── env.py
|
|||
|
|
│ └── versions/
|
|||
|
|
├── alembic.ini
|
|||
|
|
├── requirements.txt
|
|||
|
|
├── pyproject.toml
|
|||
|
|
├── Dockerfile
|
|||
|
|
└── tests/
|
|||
|
|
├── __init__.py
|
|||
|
|
├── conftest.py
|
|||
|
|
└── test_health.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 编写核心配置文件**
|
|||
|
|
|
|||
|
|
`backend/app/core/config.py` 使用 Pydantic BaseSettings:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from pydantic_settings import BaseSettings
|
|||
|
|
|
|||
|
|
class Settings(BaseSettings):
|
|||
|
|
# App
|
|||
|
|
APP_NAME: str = "AI Reimbursement Agent"
|
|||
|
|
APP_VERSION: str = "0.1.0"
|
|||
|
|
DEBUG: bool = True
|
|||
|
|
|
|||
|
|
# Database
|
|||
|
|
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/x_financial"
|
|||
|
|
|
|||
|
|
# Redis
|
|||
|
|
REDIS_URL: str = "redis://localhost:6379/0"
|
|||
|
|
|
|||
|
|
# MinIO / S3
|
|||
|
|
MINIO_ENDPOINT: str = "localhost:9000"
|
|||
|
|
MINIO_ACCESS_KEY: str = "minioadmin"
|
|||
|
|
MINIO_SECRET_KEY: str = "minioadmin"
|
|||
|
|
MINIO_BUCKET: str = "reimbursement"
|
|||
|
|
|
|||
|
|
# OCR
|
|||
|
|
OCR_PROVIDER: str = "baidu" # baidu | tencent | mock
|
|||
|
|
BAIDU_OCR_API_KEY: str = ""
|
|||
|
|
BAIDU_OCR_SECRET_KEY: str = ""
|
|||
|
|
|
|||
|
|
# LLM
|
|||
|
|
LLM_PROVIDER: str = "openai"
|
|||
|
|
LLM_API_KEY: str = ""
|
|||
|
|
LLM_MODEL: str = "gpt-4o-mini"
|
|||
|
|
LLM_BASE_URL: str = ""
|
|||
|
|
|
|||
|
|
class Config:
|
|||
|
|
env_file = ".env"
|
|||
|
|
|
|||
|
|
settings = Settings()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 编写数据库连接和 FastAPI 入口**
|
|||
|
|
|
|||
|
|
`backend/app/core/database.py`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
|||
|
|
from sqlalchemy.orm import DeclarativeBase
|
|||
|
|
from app.core.config import settings
|
|||
|
|
|
|||
|
|
engine = create_async_engine(settings.DATABASE_URL, echo=settings.DEBUG)
|
|||
|
|
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|||
|
|
|
|||
|
|
class Base(DeclarativeBase):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
async def get_db() -> AsyncSession:
|
|||
|
|
async with async_session() as session:
|
|||
|
|
yield session
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`backend/app/main.py`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from fastapi import FastAPI
|
|||
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|||
|
|
from app.core.config import settings
|
|||
|
|
from app.api.v1.router import api_router
|
|||
|
|
|
|||
|
|
app = FastAPI(title=settings.APP_NAME, version=settings.APP_VERSION)
|
|||
|
|
|
|||
|
|
app.add_middleware(
|
|||
|
|
CORSMiddleware,
|
|||
|
|
allow_origins=["*"],
|
|||
|
|
allow_credentials=True,
|
|||
|
|
allow_methods=["*"],
|
|||
|
|
allow_headers=["*"],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
app.include_router(api_router, prefix="/api/v1")
|
|||
|
|
|
|||
|
|
@app.get("/health")
|
|||
|
|
async def health():
|
|||
|
|
return {"status": "ok", "version": settings.APP_VERSION}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 编写 requirements.txt**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
fastapi==0.115.0
|
|||
|
|
uvicorn[standard]==0.30.0
|
|||
|
|
sqlalchemy[asyncio]==2.0.35
|
|||
|
|
asyncpg==0.30.0
|
|||
|
|
alembic==1.13.0
|
|||
|
|
pydantic==2.9.0
|
|||
|
|
pydantic-settings==2.5.0
|
|||
|
|
python-multipart==0.0.9
|
|||
|
|
httpx==0.27.0
|
|||
|
|
redis==5.1.0
|
|||
|
|
minio==7.2.9
|
|||
|
|
python-jose[cryptography]==3.3.0
|
|||
|
|
passlib[bcrypt]==1.7.4
|
|||
|
|
pytest==8.3.0
|
|||
|
|
pytest-asyncio==0.24.0
|
|||
|
|
httpx # for TestClient
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 编写健康检查测试**
|
|||
|
|
|
|||
|
|
`backend/tests/conftest.py`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import pytest
|
|||
|
|
from httpx import AsyncClient, ASGITransport
|
|||
|
|
from app.main import app
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
async def client():
|
|||
|
|
transport = ASGITransport(app=app)
|
|||
|
|
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|||
|
|
yield ac
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`backend/tests/test_health.py`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_health(client):
|
|||
|
|
response = await client.get("/health")
|
|||
|
|
assert response.status_code == 200
|
|||
|
|
data = response.json()
|
|||
|
|
assert data["status"] == "ok"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: 运行测试确认骨架可用**
|
|||
|
|
|
|||
|
|
Run: `cd backend && pip install -r requirements.txt && pytest tests/test_health.py -v`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 初始化后端项目骨架(FastAPI + SQLAlchemy + Alembic)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 1.2: 数据库 Schema + 迁移
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/models/base.py`
|
|||
|
|
- Create: `backend/app/models/task.py`
|
|||
|
|
- Create: `backend/app/models/reimbursement.py`
|
|||
|
|
- Create: `backend/app/models/document.py`
|
|||
|
|
- Create: `backend/app/models/rule.py`
|
|||
|
|
- Create: `backend/app/models/audit.py`
|
|||
|
|
- Create: `backend/app/models/enums.py`
|
|||
|
|
- Modify: `backend/app/models/__init__.py`
|
|||
|
|
- Test: `backend/tests/test_models.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 定义枚举类型**
|
|||
|
|
|
|||
|
|
`backend/app/models/enums.py`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import enum
|
|||
|
|
|
|||
|
|
class TaskStatus(str, enum.Enum):
|
|||
|
|
CREATED = "created"
|
|||
|
|
MATERIAL_COLLECTING = "material_collecting"
|
|||
|
|
PARSING = "parsing"
|
|||
|
|
DRAFT_GENERATED = "draft_generated"
|
|||
|
|
PRECHECKING = "prechecking"
|
|||
|
|
NEED_SUPPLEMENT = "need_supplement"
|
|||
|
|
PENDING_USER_CONFIRM = "pending_user_confirm"
|
|||
|
|
SUBMITTING = "submitting"
|
|||
|
|
SYNCED = "synced"
|
|||
|
|
SYNC_FAILED = "sync_failed"
|
|||
|
|
CLOSED = "closed"
|
|||
|
|
|
|||
|
|
class ExpenseType(str, enum.Enum):
|
|||
|
|
TRAVEL_TRANSPORT = "travel_transport"
|
|||
|
|
TRAVEL_HOTEL = "travel_hotel"
|
|||
|
|
TRAVEL_MEAL = "travel_meal"
|
|||
|
|
LOCAL_TRANSPORT = "local_transport"
|
|||
|
|
BUSINESS_MEAL = "business_meal"
|
|||
|
|
OFFICE_SUPPLY = "office_supply"
|
|||
|
|
COMMUNICATION = "communication"
|
|||
|
|
OTHER = "other"
|
|||
|
|
|
|||
|
|
class DocumentType(str, enum.Enum):
|
|||
|
|
VAT_INVOICE = "vat_invoice"
|
|||
|
|
TRAIN_TICKET = "train_ticket"
|
|||
|
|
FLIGHT_ITINERARY = "flight_itinerary"
|
|||
|
|
TAXI_RECEIPT = "taxi_receipt"
|
|||
|
|
HOTEL_BILL = "hotel_bill"
|
|||
|
|
PAYMENT_SCREENSHOT = "payment_screenshot"
|
|||
|
|
TRAVEL_ORDER = "travel_order"
|
|||
|
|
OTHER_ATTACHMENT = "other_attachment"
|
|||
|
|
|
|||
|
|
class RiskLevel(str, enum.Enum):
|
|||
|
|
LOW = "low"
|
|||
|
|
MEDIUM = "medium"
|
|||
|
|
HIGH = "high"
|
|||
|
|
BLOCKED = "blocked"
|
|||
|
|
|
|||
|
|
class RuleAction(str, enum.Enum):
|
|||
|
|
PASS = "pass"
|
|||
|
|
WARN = "warn"
|
|||
|
|
REQUIRE_EXPLANATION = "require_explanation"
|
|||
|
|
REQUIRE_ATTACHMENT = "require_attachment"
|
|||
|
|
REQUIRE_APPROVAL = "require_approval"
|
|||
|
|
BLOCK = "block"
|
|||
|
|
|
|||
|
|
class SyncStatus(str, enum.Enum):
|
|||
|
|
SUCCESS = "success"
|
|||
|
|
FAILED = "failed"
|
|||
|
|
RETRYING = "retrying"
|
|||
|
|
PENDING = "pending"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 定义所有 ORM 模型**
|
|||
|
|
|
|||
|
|
`backend/app/models/base.py`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import uuid
|
|||
|
|
from datetime import datetime
|
|||
|
|
from sqlalchemy import String, DateTime, func
|
|||
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|||
|
|
|
|||
|
|
def generate_id():
|
|||
|
|
return str(uuid.uuid4())
|
|||
|
|
|
|||
|
|
class TimestampMixin:
|
|||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|||
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
|
|||
|
|
|
|||
|
|
class IDMixin:
|
|||
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_id)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`backend/app/models/task.py` — 包含 `ReimbursementTask`,字段按文档 5.2.1 节:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from sqlalchemy import String, Text, ForeignKey
|
|||
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||
|
|
from app.models.base import IDMixin, TimestampMixin, Base
|
|||
|
|
from app.models.enums import TaskStatus
|
|||
|
|
|
|||
|
|
class ReimbursementTask(IDMixin, TimestampMixin, Base):
|
|||
|
|
__tablename__ = "reimbursement_task"
|
|||
|
|
|
|||
|
|
user_id: Mapped[str] = mapped_column(String(36), index=True)
|
|||
|
|
company_id: Mapped[str] = mapped_column(String(36), index=True)
|
|||
|
|
task_type: Mapped[str] = mapped_column(String(50), default="travel_expense")
|
|||
|
|
status: Mapped[TaskStatus] = mapped_column(default=TaskStatus.CREATED)
|
|||
|
|
user_intent: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|||
|
|
current_agent: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|||
|
|
|
|||
|
|
# relationships
|
|||
|
|
documents = relationship("ExpenseDocument", back_populates="task", lazy="selectin")
|
|||
|
|
reimbursement = relationship("ShadowReimbursement", back_populates="task", uselist=False, lazy="selectin")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`backend/app/models/reimbursement.py` — 包含 `ShadowReimbursement`, `ReimbursementItem`, `SupplementRequest`, `SyncRecord`,字段按文档 5.2.2 ~ 5.2.4, 5.2.7, 5.2.8 节定义。
|
|||
|
|
|
|||
|
|
`backend/app/models/document.py` — `ExpenseDocument`,字段按文档 5.2.4 节。
|
|||
|
|
|
|||
|
|
`backend/app/models/rule.py` — `ExpenseRule`, `RuleHit`,字段按文档 5.2.5, 5.2.6 节。
|
|||
|
|
|
|||
|
|
`backend/app/models/audit.py` — `AuditLog`(通用审计日志表,记录所有关键操作)。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 更新 `models/__init__.py` 导出所有模型**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from app.models.task import ReimbursementTask
|
|||
|
|
from app.models.reimbursement import ShadowReimbursement, ReimbursementItem, SupplementRequest, SyncRecord
|
|||
|
|
from app.models.document import ExpenseDocument
|
|||
|
|
from app.models.rule import ExpenseRule, RuleHit
|
|||
|
|
from app.models.audit import AuditLog
|
|||
|
|
from app.models.base import Base
|
|||
|
|
|
|||
|
|
__all__ = [
|
|||
|
|
"ReimbursementTask", "ShadowReimbursement", "ReimbursementItem",
|
|||
|
|
"ExpenseDocument", "ExpenseRule", "RuleHit",
|
|||
|
|
"SupplementRequest", "SyncRecord", "AuditLog", "Base",
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 生成 Alembic 迁移**
|
|||
|
|
|
|||
|
|
Run: `cd backend && alembic revision --autogenerate -m "init schema"`
|
|||
|
|
Run: `cd backend && alembic upgrade head`
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 编写模型测试**
|
|||
|
|
|
|||
|
|
验证所有表能正确创建和插入数据。
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 完成所有数据模型定义和数据库迁移"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 1.3: 前端项目骨架搭建
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: 使用 Vite 初始化 Vue3 + TypeScript 项目
|
|||
|
|
- Create: `frontend/src/router/index.ts`
|
|||
|
|
- Create: `frontend/src/stores/` — Pinia stores
|
|||
|
|
- Create: `frontend/src/api/` — API 调用封装
|
|||
|
|
- Create: `frontend/src/views/` — 页面
|
|||
|
|
- Create: `frontend/src/components/` — 组件
|
|||
|
|
- Create: `frontend/src/layouts/` — 布局
|
|||
|
|
- Test: `frontend/src/App.vue`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 初始化 Vue3 项目**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm create vite@latest frontend -- --template vue-ts
|
|||
|
|
cd frontend
|
|||
|
|
npm install ant-design-vue @ant-design/icons-vue vue-router pinia axios dayjs
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
目录结构:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
frontend/
|
|||
|
|
├── src/
|
|||
|
|
│ ├── api/ # API 调用
|
|||
|
|
│ │ ├── index.ts # axios 实例
|
|||
|
|
│ │ ├── task.ts # 报销任务 API
|
|||
|
|
│ │ ├── document.ts # 票据 API
|
|||
|
|
│ │ └── precheck.ts # 预审 API
|
|||
|
|
│ ├── components/ # 通用组件
|
|||
|
|
│ │ ├── FileUpload.vue
|
|||
|
|
│ │ ├── ExpenseTable.vue
|
|||
|
|
│ │ └── RuleHitCard.vue
|
|||
|
|
│ ├── layouts/
|
|||
|
|
│ │ └── MainLayout.vue
|
|||
|
|
│ ├── router/
|
|||
|
|
│ │ └── index.ts
|
|||
|
|
│ ├── stores/
|
|||
|
|
│ │ ├── task.ts
|
|||
|
|
│ │ └── user.ts
|
|||
|
|
│ ├── views/
|
|||
|
|
│ │ ├── HomeView.vue # 报销入口
|
|||
|
|
│ │ ├── UploadView.vue # 票据上传
|
|||
|
|
│ │ ├── DraftView.vue # 报销草稿
|
|||
|
|
│ │ ├── PrecheckView.vue # 预审结果
|
|||
|
|
│ │ ├── SupplementView.vue # 补件交互
|
|||
|
|
│ │ ├── ConfirmView.vue # 提交确认
|
|||
|
|
│ │ └── AuditView.vue # 审计日志
|
|||
|
|
│ ├── App.vue
|
|||
|
|
│ └── main.ts
|
|||
|
|
├── index.html
|
|||
|
|
├── vite.config.ts
|
|||
|
|
├── tsconfig.json
|
|||
|
|
└── package.json
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 配置路由和布局**
|
|||
|
|
|
|||
|
|
`frontend/src/router/index.ts`:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { createRouter, createWebHistory } from 'vue-router'
|
|||
|
|
|
|||
|
|
const routes = [
|
|||
|
|
{ path: '/', name: 'home', component: () => import('@/views/HomeView.vue') },
|
|||
|
|
{ path: '/task/:taskId/upload', name: 'upload', component: () => import('@/views/UploadView.vue') },
|
|||
|
|
{ path: '/task/:taskId/draft', name: 'draft', component: () => import('@/views/DraftView.vue') },
|
|||
|
|
{ path: '/task/:taskId/precheck', name: 'precheck', component: () => import('@/views/PrecheckView.vue') },
|
|||
|
|
{ path: '/task/:taskId/supplement', name: 'supplement', component: () => import('@/views/SupplementView.vue') },
|
|||
|
|
{ path: '/task/:taskId/confirm', name: 'confirm', component: () => import('@/views/ConfirmView.vue') },
|
|||
|
|
{ path: '/audit', name: 'audit', component: () => import('@/views/AuditView.vue') },
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
export default createRouter({ history: createWebHistory(), routes })
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 配置 API 封装**
|
|||
|
|
|
|||
|
|
`frontend/src/api/index.ts`:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import axios from 'axios'
|
|||
|
|
|
|||
|
|
const api = axios.create({
|
|||
|
|
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api/v1',
|
|||
|
|
timeout: 30000,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export default api
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 确认前端能正常启动**
|
|||
|
|
|
|||
|
|
Run: `cd frontend && npm run dev`
|
|||
|
|
Expected: 浏览器访问 http://localhost:5173 能看到页面
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add frontend/
|
|||
|
|
git commit -m "feat: 初始化前端项目骨架(Vue3 + TypeScript + Ant Design Vue)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 1.4: Docker Compose 开发环境
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `docker-compose.yml`
|
|||
|
|
- Create: `backend/Dockerfile`
|
|||
|
|
- Create: `frontend/Dockerfile`
|
|||
|
|
- Create: `.env.example`
|
|||
|
|
- Create: `backend/init.sql`(初始数据)
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 编写 docker-compose.yml**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
version: "3.8"
|
|||
|
|
services:
|
|||
|
|
postgres:
|
|||
|
|
image: postgres:15
|
|||
|
|
environment:
|
|||
|
|
POSTGRES_DB: x_financial
|
|||
|
|
POSTGRES_USER: postgres
|
|||
|
|
POSTGRES_PASSWORD: postgres
|
|||
|
|
ports:
|
|||
|
|
- "5432:5432"
|
|||
|
|
volumes:
|
|||
|
|
- pgdata:/var/lib/postgresql/data
|
|||
|
|
|
|||
|
|
redis:
|
|||
|
|
image: redis:7-alpine
|
|||
|
|
ports:
|
|||
|
|
- "6379:6379"
|
|||
|
|
|
|||
|
|
minio:
|
|||
|
|
image: minio/minio
|
|||
|
|
command: server /data --console-address ":9001"
|
|||
|
|
environment:
|
|||
|
|
MINIO_ROOT_USER: minioadmin
|
|||
|
|
MINIO_ROOT_PASSWORD: minioadmin
|
|||
|
|
ports:
|
|||
|
|
- "9000:9000"
|
|||
|
|
- "9001:9001"
|
|||
|
|
volumes:
|
|||
|
|
- minio_data:/data
|
|||
|
|
|
|||
|
|
volumes:
|
|||
|
|
pgdata:
|
|||
|
|
minio_data:
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 编写 .env.example**
|
|||
|
|
|
|||
|
|
按 config.py 中的字段列出所有环境变量,标注必填/选填。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 验证环境启动**
|
|||
|
|
|
|||
|
|
Run: `docker-compose up -d`
|
|||
|
|
Run: `docker-compose ps`
|
|||
|
|
Expected: postgres, redis, minio 均为 running
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add docker-compose.yml .env.example backend/Dockerfile frontend/Dockerfile
|
|||
|
|
git commit -m "feat: 添加 Docker Compose 开发环境(PostgreSQL + Redis + MinIO)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 2: 后端核心服务(W2-W3)
|
|||
|
|
|
|||
|
|
### Task 2.1: 报销任务管理 API
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/schemas/task.py`
|
|||
|
|
- Create: `backend/app/services/task_service.py`
|
|||
|
|
- Create: `backend/app/api/v1/tasks.py`
|
|||
|
|
- Modify: `backend/app/api/v1/router.py`
|
|||
|
|
- Test: `backend/tests/test_task_api.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 定义 Pydantic schemas**
|
|||
|
|
|
|||
|
|
`backend/app/schemas/task.py` — 包含 `TaskCreateRequest`, `TaskResponse`, `TaskListResponse` 等请求/响应模型,字段按文档第 8 节 API 定义。
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现 TaskService 业务逻辑**
|
|||
|
|
|
|||
|
|
包含方法:
|
|||
|
|
- `create_task(user_id, company_id, user_intent)` → 创建任务,状态设为 `CREATED`
|
|||
|
|
- `get_task(task_id)` → 查询任务详情
|
|||
|
|
- `list_tasks(user_id, status, page, size)` → 分页查询
|
|||
|
|
- `update_status(task_id, status, current_agent)` → 更新任务状态
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现 API 路由**
|
|||
|
|
|
|||
|
|
`POST /api/v1/reimbursement/tasks` — 创建报销任务(对应文档 8.1)
|
|||
|
|
`GET /api/v1/reimbursement/tasks/{task_id}` — 查询任务详情
|
|||
|
|
`GET /api/v1/reimbursement/tasks` — 列表查询
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 编写测试**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_create_task(client):
|
|||
|
|
response = await client.post("/api/v1/reimbursement/tasks", json={
|
|||
|
|
"user_id": "U001",
|
|||
|
|
"company_id": "C001",
|
|||
|
|
"user_intent": "我要报这次北京出差的费用",
|
|||
|
|
"entry_channel": "web"
|
|||
|
|
})
|
|||
|
|
assert response.status_code == 201
|
|||
|
|
data = response.json()
|
|||
|
|
assert "task_id" in data
|
|||
|
|
assert data["status"] == "material_collecting"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 实现报销任务管理 API(创建/查询/列表)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 2.2: 文件上传与票据管理 API
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/schemas/document.py`
|
|||
|
|
- Create: `backend/app/services/document_service.py`
|
|||
|
|
- Create: `backend/app/services/storage_service.py`
|
|||
|
|
- Create: `backend/app/api/v1/documents.py`
|
|||
|
|
- Modify: `backend/app/api/v1/router.py`
|
|||
|
|
- Test: `backend/tests/test_document_api.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 实现 MinIO 存储服务**
|
|||
|
|
|
|||
|
|
`storage_service.py` — 封装 MinIO 操作:
|
|||
|
|
- `upload_file(bucket, file_name, file_data, content_type)` → 上传文件
|
|||
|
|
- `get_file_url(bucket, file_name)` → 获取文件 URL
|
|||
|
|
- `delete_file(bucket, file_name)` → 删除文件
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现文档服务**
|
|||
|
|
|
|||
|
|
`document_service.py`:
|
|||
|
|
- `upload_document(task_id, file, document_type)` → 保存文件到 MinIO,创建 DB 记录
|
|||
|
|
- `get_documents(task_id)` → 查询任务下所有票据
|
|||
|
|
- `update_ocr_result(document_id, ocr_result)` → 更新 OCR 识别结果
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现 API 路由**
|
|||
|
|
|
|||
|
|
`POST /api/v1/reimbursement/tasks/{task_id}/documents` — 上传票据(对应文档 8.2)
|
|||
|
|
`GET /api/v1/reimbursement/tasks/{task_id}/documents` — 查询票据列表
|
|||
|
|
|
|||
|
|
支持 multipart/form-data 文件上传。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 编写测试**
|
|||
|
|
|
|||
|
|
使用 mock MinIO,测试文件上传和记录创建。
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 实现文件上传与票据管理 API(MinIO 存储)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 2.3: OCR 服务集成
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/services/ocr_service.py`
|
|||
|
|
- Create: `backend/app/services/ocr_providers/base.py`
|
|||
|
|
- Create: `backend/app/services/ocr_providers/baidu.py`
|
|||
|
|
- Create: `backend/app/services/ocr_providers/mock.py`
|
|||
|
|
- Test: `backend/tests/test_ocr_service.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 定义 OCR Provider 抽象接口**
|
|||
|
|
|
|||
|
|
`ocr_providers/base.py`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from abc import ABC, abstractmethod
|
|||
|
|
from dataclasses import dataclass
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class OCRResult:
|
|||
|
|
document_type: str # 识别出的票据类型
|
|||
|
|
raw_text: str # 原始文字
|
|||
|
|
fields: dict # 结构化字段
|
|||
|
|
confidence: float # 整体置信度
|
|||
|
|
provider: str # 提供商
|
|||
|
|
|
|||
|
|
class OCRProvider(ABC):
|
|||
|
|
@abstractmethod
|
|||
|
|
async def recognize(self, file_url: str, document_type: str | None = None) -> OCRResult:
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现 Mock OCR Provider(开发测试用)**
|
|||
|
|
|
|||
|
|
`mock.py` — 根据文件名/类型返回预定义的结构化数据,用于开发阶段不依赖真实 OCR。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现百度 OCR Provider**
|
|||
|
|
|
|||
|
|
`baidu.py` — 调用百度云 OCR API,支持:
|
|||
|
|
- 增值税发票识别
|
|||
|
|
- 火车票识别
|
|||
|
|
- 机票行程单识别
|
|||
|
|
- 通用票据识别(兜底)
|
|||
|
|
|
|||
|
|
将百度返回结果标准化为 `OCRResult`。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 实现 OCR Service 门面**
|
|||
|
|
|
|||
|
|
`ocr_service.py`:
|
|||
|
|
- `recognize(file_url, document_type)` → 根据 `config.OCR_PROVIDER` 选择 provider
|
|||
|
|
- 自动识别票据类型(如果未指定)
|
|||
|
|
- 返回标准化 `OCRResult`
|
|||
|
|
- 更新 document 表的 `ocr_status` 和 `extracted_json`
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 编写测试**
|
|||
|
|
|
|||
|
|
使用 Mock Provider 测试完整流程。
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 实现 OCR 服务(百度云 + Mock Provider)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 2.4: 规则引擎
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/services/rule_engine.py`
|
|||
|
|
- Create: `backend/app/schemas/rule.py`
|
|||
|
|
- Create: `backend/app/api/v1/rules.py`
|
|||
|
|
- Create: `backend/app/services/rule_checkers/__init__.py`
|
|||
|
|
- Create: `backend/app/services/rule_checkers/required_fields.py`
|
|||
|
|
- Create: `backend/app/services/rule_checkers/attachment_check.py`
|
|||
|
|
- Create: `backend/app/services/rule_checkers/duplicate_invoice.py`
|
|||
|
|
- Create: `backend/app/services/rule_checkers/amount_limit.py`
|
|||
|
|
- Create: `backend/app/services/rule_checkers/date_validity.py`
|
|||
|
|
- Create: `backend/app/services/rule_checkers/expense_type_match.py`
|
|||
|
|
- Test: `backend/tests/test_rule_engine.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 定义规则引擎核心接口**
|
|||
|
|
|
|||
|
|
`rule_engine.py` — `RuleEngine` 类:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class RuleCheckResult:
|
|||
|
|
rule_code: str
|
|||
|
|
severity: RiskLevel
|
|||
|
|
action: RuleAction
|
|||
|
|
message: str
|
|||
|
|
suggestion: str
|
|||
|
|
policy_ref: str
|
|||
|
|
hit_detail: dict
|
|||
|
|
|
|||
|
|
class RuleEngine:
|
|||
|
|
def __init__(self, db: AsyncSession):
|
|||
|
|
self.db = db
|
|||
|
|
self.checkers: dict[str, RuleChecker] = {}
|
|||
|
|
|
|||
|
|
async def run_precheck(self, reimbursement_id: str) -> PrecheckResult:
|
|||
|
|
"""执行完整预审"""
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
async def run_single_rule(self, rule_code: str, context: dict) -> RuleCheckResult | None:
|
|||
|
|
"""执行单条规则"""
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现 MVP 阶段的 6 条核心规则检查器**
|
|||
|
|
|
|||
|
|
每条规则是一个 `RuleChecker` 类,接收报销数据上下文,返回 `RuleCheckResult | None`:
|
|||
|
|
|
|||
|
|
1. `RequiredFieldsChecker` — 必填字段校验
|
|||
|
|
2. `AttachmentCheckChecker` — 附件完整性校验(如住宿费必须上传酒店流水)
|
|||
|
|
3. `DuplicateInvoiceChecker` — 重复发票检查(invoice_code + invoice_number + amount 去重)
|
|||
|
|
4. `AmountLimitChecker` — 金额超标校验(按城市/职级/费用类型检查标准)
|
|||
|
|
5. `DateValidityChecker` — 日期合理性校验(费用日期在出差期间内)
|
|||
|
|
6. `ExpenseTypeMatchChecker` — 费用类型与票据类型匹配校验
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现规则管理 API**
|
|||
|
|
|
|||
|
|
- `GET /api/v1/rules` — 列出所有规则
|
|||
|
|
- `POST /api/v1/rules` — 创建规则
|
|||
|
|
- `PUT /api/v1/rules/{rule_id}` — 更新规则
|
|||
|
|
- `PATCH /api/v1/rules/{rule_id}/toggle` — 启用/禁用规则
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 种子数据**
|
|||
|
|
|
|||
|
|
`backend/alembic/seed/rules.sql` — 预置文档 7.2 节的 3 条示例规则 + 金额标准表。
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 编写测试**
|
|||
|
|
|
|||
|
|
对每条规则编写单元测试,使用 mock 数据验证命中/未命中场景。
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 实现规则引擎(6 条核心规则 + 管理 API + 种子数据)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 2.5: 影子报销账本 CRUD
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/schemas/reimbursement.py`
|
|||
|
|
- Create: `backend/app/services/ledger_service.py`
|
|||
|
|
- Create: `backend/app/api/v1/ledger.py`
|
|||
|
|
- Modify: `backend/app/api/v1/router.py`
|
|||
|
|
- Test: `backend/tests/test_ledger_api.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 定义 Pydantic schemas**
|
|||
|
|
|
|||
|
|
包含:`ReimbursementDraftResponse`, `ReimbursementItemCreate`, `ReimbursementItemResponse`, `PrecheckResultResponse`。
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现 LedgerService**
|
|||
|
|
|
|||
|
|
核心方法:
|
|||
|
|
- `create_shadow_reimbursement(task_id, data)` → 创建影子报销记录
|
|||
|
|
- `get_draft(reimbursement_id)` → 获取报销草稿
|
|||
|
|
- `get_draft_by_task(task_id)` → 通过任务 ID 获取草稿
|
|||
|
|
- `update_precheck_result(reimbursement_id, result)` → 更新预审结果
|
|||
|
|
- `add_item(reimbursement_id, item_data)` → 添加报销明细
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现 API 路由**
|
|||
|
|
|
|||
|
|
`GET /api/v1/reimbursement/tasks/{task_id}/draft` — 获取报销草稿(对应文档 8.4)
|
|||
|
|
`GET /api/v1/reimbursement/tasks/{task_id}/precheck-result` — 获取预审结果(对应文档 8.5)
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 编写测试**
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 实现影子报销账本 CRUD API"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 2.6: 补件与提交 API
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/api/v1/supplements.py`
|
|||
|
|
- Create: `backend/app/services/supplement_service.py`
|
|||
|
|
- Create: `backend/app/services/sync_service.py`
|
|||
|
|
- Modify: `backend/app/api/v1/router.py`
|
|||
|
|
- Test: `backend/tests/test_supplement_api.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 实现补件服务**
|
|||
|
|
|
|||
|
|
`supplement_service.py`:
|
|||
|
|
- `create_supplement_request(reimbursement_id, items)` → 创建补件请求
|
|||
|
|
- `respond_supplement(request_id, response_text, document_ids)` → 用户补件响应
|
|||
|
|
- `get_supplement_requests(task_id)` → 查询补件请求列表
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现同步服务(MVP 阶段为模拟)**
|
|||
|
|
|
|||
|
|
`sync_service.py`:
|
|||
|
|
- `mock_sync_to_backend(reimbursement_id)` → 模拟后端同步,生成假的 backend_bill_id
|
|||
|
|
- `get_sync_status(task_id)` → 查询同步状态
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现 API 路由**
|
|||
|
|
|
|||
|
|
`POST /api/v1/reimbursement/tasks/{task_id}/supplements` — 用户补件(对应文档 8.6)
|
|||
|
|
`POST /api/v1/reimbursement/tasks/{task_id}/submit` — 用户确认提交(对应文档 8.7)
|
|||
|
|
`GET /api/v1/reimbursement/tasks/{task_id}/sync-status` — 查询同步状态(对应文档 8.8)
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 编写测试**
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 实现补件与提交确认 API(含模拟同步)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 3: Agent 编排(W3-W4)
|
|||
|
|
|
|||
|
|
### Task 3.1: Agent Orchestrator 状态机
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/agents/orchestrator.py`
|
|||
|
|
- Create: `backend/app/agents/state.py`
|
|||
|
|
- Create: `backend/app/api/v1/agent.py`
|
|||
|
|
- Modify: `backend/app/api/v1/router.py`
|
|||
|
|
- Test: `backend/tests/test_orchestrator.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 定义 Agent 状态和上下文**
|
|||
|
|
|
|||
|
|
`agents/state.py`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from dataclasses import dataclass, field
|
|||
|
|
from app.models.enums import TaskStatus
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class AgentContext:
|
|||
|
|
task_id: str
|
|||
|
|
status: TaskStatus
|
|||
|
|
user_intent: str | None = None
|
|||
|
|
current_agent: str | None = None
|
|||
|
|
ocr_results: list[dict] = field(default_factory=list)
|
|||
|
|
reimbursement_data: dict | None = None
|
|||
|
|
precheck_result: dict | None = None
|
|||
|
|
supplement_requests: list[dict] = field(default_factory=list)
|
|||
|
|
error_message: str | None = None
|
|||
|
|
retry_count: int = 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现 Orchestrator 状态机**
|
|||
|
|
|
|||
|
|
`agents/orchestrator.py` — 核心编排逻辑:
|
|||
|
|
|
|||
|
|
状态转换图(对应文档 4.2 节):
|
|||
|
|
```
|
|||
|
|
CREATED → MATERIAL_COLLECTING → PARSING → DRAFT_GENERATED → PRECHECKING
|
|||
|
|
→ NEED_SUPPLEMENT → MATERIAL_COLLECTING(循环)
|
|||
|
|
→ PENDING_USER_CONFIRM → SUBMITTING → SYNCED
|
|||
|
|
→ SYNC_FAILED → SUBMITTING(重试)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
方法:
|
|||
|
|
- `run(task_id, start_from)` → 启动编排
|
|||
|
|
- `_transition_to(context, new_status, agent_name)` → 状态转换
|
|||
|
|
- `_run_agent(context, agent_name)` → 执行单个 Agent
|
|||
|
|
- `_handle_agent_result(context, result)` → 处理 Agent 返回结果
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现 Agent 启动 API**
|
|||
|
|
|
|||
|
|
`POST /api/v1/reimbursement/tasks/{task_id}/agent/run` — 启动 Agent 处理(对应文档 8.3)
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 编写状态机转换测试**
|
|||
|
|
|
|||
|
|
覆盖所有正常路径和异常路径(解析失败、预审需补件、同步失败重试等)。
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 实现 Agent Orchestrator 状态机编排"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 3.2: 5 个 Agent 实现
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/agents/base_agent.py`
|
|||
|
|
- Create: `backend/app/agents/intake_agent.py`
|
|||
|
|
- Create: `backend/app/agents/parse_agent.py`
|
|||
|
|
- Create: `backend/app/agents/rule_check_agent.py`
|
|||
|
|
- Create: `backend/app/agents/explain_agent.py`
|
|||
|
|
- Create: `backend/app/agents/sync_agent.py`
|
|||
|
|
- Test: `backend/tests/test_agents.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 定义 Agent 基类**
|
|||
|
|
|
|||
|
|
`agents/base_agent.py`:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from abc import ABC, abstractmethod
|
|||
|
|
from app.agents.state import AgentContext
|
|||
|
|
|
|||
|
|
class AgentResult:
|
|||
|
|
success: bool
|
|||
|
|
data: dict
|
|||
|
|
next_action: str | None # 继续编排 / 等待用户 / 需要补件
|
|||
|
|
error: str | None
|
|||
|
|
|
|||
|
|
class BaseAgent(ABC):
|
|||
|
|
name: str
|
|||
|
|
|
|||
|
|
@abstractmethod
|
|||
|
|
async def execute(self, context: AgentContext, db: AsyncSession) -> AgentResult:
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现受理 Agent(IntakeAgent)**
|
|||
|
|
|
|||
|
|
职责:理解用户意图,收集上下文。
|
|||
|
|
- 分析 user_intent 文本,提取报销类型、出差信息
|
|||
|
|
- 调用 LLM 做 intent classification
|
|||
|
|
- 返回结构化的任务信息
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现单据解析 Agent(ParseAgent)**
|
|||
|
|
|
|||
|
|
职责:调用 OCR,生成报销草稿。
|
|||
|
|
- 遍历任务下的所有 document,调用 ocr_service
|
|||
|
|
- 将 OCR 结果汇总为报销明细
|
|||
|
|
- 创建 ShadowReimbursement + ReimbursementItem
|
|||
|
|
- 自动识别费用类型
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 实现规则校验 Agent(RuleCheckAgent)**
|
|||
|
|
|
|||
|
|
职责:调用规则引擎完成预审。
|
|||
|
|
- 从 DB 加载所有 enabled 规则
|
|||
|
|
- 传入报销数据上下文执行规则引擎
|
|||
|
|
- 收集所有 RuleHit
|
|||
|
|
- 计算 overall risk_level
|
|||
|
|
- 更新预审状态
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 实现解释与补件 Agent(ExplainAgent)**
|
|||
|
|
|
|||
|
|
职责:将规则命中结果转化为用户可理解的解释。
|
|||
|
|
- 遍历 rule_hits,使用 LLM 生成自然语言解释
|
|||
|
|
- 创建 supplement_requests(缺件类型的自动创建补件请求)
|
|||
|
|
- 生成修改建议
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: 实现同步执行 Agent(SyncAgent)**
|
|||
|
|
|
|||
|
|
职责:生成标准报销单,调用后端同步。
|
|||
|
|
- 将 ShadowReimbursement 数据映射为标准报销单格式
|
|||
|
|
- 调用 sync_service.mock_sync_to_backend
|
|||
|
|
- 记录 SyncRecord
|
|||
|
|
- 处理同步失败重试
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: 编写每个 Agent 的单元测试**
|
|||
|
|
|
|||
|
|
使用 mock DB 和 mock OCR/LLM 测试每个 Agent 的输入输出。
|
|||
|
|
|
|||
|
|
- [ ] **Step 8: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 实现 5 个 Agent(受理/解析/规则校验/解释补件/同步)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 3.3: LLM 集成层
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/services/llm_service.py`
|
|||
|
|
- Create: `backend/app/services/llm_prompts/__init__.py`
|
|||
|
|
- Create: `backend/app/services/llm_prompts/intent_classification.py`
|
|||
|
|
- Create: `backend/app/services/llm_prompts/risk_explanation.py`
|
|||
|
|
- Create: `backend/app/services/llm_prompts/expense_type_mapping.py`
|
|||
|
|
- Test: `backend/tests/test_llm_service.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 实现 LLM Service 封装**
|
|||
|
|
|
|||
|
|
`llm_service.py`:
|
|||
|
|
- `chat(system_prompt, user_message, json_mode)` → 调用 LLM API
|
|||
|
|
- 支持多 provider(OpenAI 兼容接口,适配国内模型)
|
|||
|
|
- 统一错误处理和重试
|
|||
|
|
- 响应解析(JSON mode)
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 编写 Prompt 模板**
|
|||
|
|
|
|||
|
|
3 个核心 Prompt:
|
|||
|
|
- `intent_classification` — 分析用户意图,识别报销类型
|
|||
|
|
- `risk_explanation` — 将规则命中结果转为自然语言解释
|
|||
|
|
- `expense_type_mapping` — 根据 OCR 结果匹配费用类型
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 编写测试(使用 mock LLM 响应)**
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 实现 LLM 集成层(多 Provider + Prompt 模板)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 3.4: 审计日志
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/app/services/audit_service.py`
|
|||
|
|
- Create: `backend/app/api/v1/audit.py`
|
|||
|
|
- Test: `backend/tests/test_audit.py`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 实现 AuditService**
|
|||
|
|
|
|||
|
|
核心方法:
|
|||
|
|
- `log(action, actor, target_type, target_id, detail)` → 记录审计日志
|
|||
|
|
- `query_logs(target_type, target_id, actor, date_range)` → 查询日志
|
|||
|
|
|
|||
|
|
需要记录的动作(对应文档 12.1 节):文件上传、OCR 识别、Agent 调用、规则命中、用户补件、用户确认、后端同步。
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 在所有关键路径埋点**
|
|||
|
|
|
|||
|
|
在 task_service、document_service、ocr_service、rule_engine、orchestrator 的关键操作中调用 `audit_service.log()`。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现审计日志查询 API**
|
|||
|
|
|
|||
|
|
`GET /api/v1/audit/logs` — 查询审计日志(支持按 target_type、target_id、date_range 过滤)
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 编写测试**
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 实现审计日志服务(记录 + 查询 API)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 4: 前端核心页面(W4-W5)
|
|||
|
|
|
|||
|
|
### Task 4.1: 报销入口页 + 上传组件
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `frontend/src/views/HomeView.vue`
|
|||
|
|
- Create: `frontend/src/views/UploadView.vue`
|
|||
|
|
- Create: `frontend/src/components/FileUpload.vue`
|
|||
|
|
- Create: `frontend/src/stores/task.ts`
|
|||
|
|
- Create: `frontend/src/api/task.ts`
|
|||
|
|
- Create: `frontend/src/api/document.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 实现 API 调用层**
|
|||
|
|
|
|||
|
|
`api/task.ts`:
|
|||
|
|
```typescript
|
|||
|
|
import api from './index'
|
|||
|
|
|
|||
|
|
export const createTask = (data: { userId: string; companyId: string; userIntent: string }) =>
|
|||
|
|
api.post('/reimbursement/tasks', data)
|
|||
|
|
|
|||
|
|
export const getTask = (taskId: string) =>
|
|||
|
|
api.get(`/reimbursement/tasks/${taskId}`)
|
|||
|
|
|
|||
|
|
export const runAgent = (taskId: string, startFrom = 'intake') =>
|
|||
|
|
api.post(`/reimbursement/tasks/${taskId}/agent/run`, { start_from: startFrom, mode: 'precheck' })
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`api/document.ts`:
|
|||
|
|
```typescript
|
|||
|
|
export const uploadDocument = (taskId: string, file: File, documentType: string) => {
|
|||
|
|
const formData = new FormData()
|
|||
|
|
formData.append('file', file)
|
|||
|
|
formData.append('document_type', documentType)
|
|||
|
|
return api.post(`/reimbursement/tasks/${taskId}/documents`, formData)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现报销入口页 HomeView**
|
|||
|
|
|
|||
|
|
按文档 9.2 节:
|
|||
|
|
- 对话输入框(用户输入报销意图)
|
|||
|
|
- 上传按钮
|
|||
|
|
- 最近任务列表
|
|||
|
|
- 常用报销类型快捷按钮("报差旅"、"看发票能不能报"等)
|
|||
|
|
- 智能引导提示
|
|||
|
|
|
|||
|
|
交互流程:用户输入意图 → 调用 createTask → 跳转到上传页
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现文件上传组件 FileUpload**
|
|||
|
|
|
|||
|
|
- 支持拖拽上传
|
|||
|
|
- 支持多文件
|
|||
|
|
- 文件类型校验(PDF、JPG、PNG)
|
|||
|
|
- 文件大小限制
|
|||
|
|
- 上传进度条
|
|||
|
|
- 预览缩略图
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 实现票据上传页 UploadView**
|
|||
|
|
|
|||
|
|
- 引用 FileUpload 组件
|
|||
|
|
- 选择票据类型下拉框(增值税发票、火车票、机票行程单等)
|
|||
|
|
- 已上传文件列表
|
|||
|
|
- "开始识别" 按钮 → 调用 runAgent → 跳转到草稿页
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add frontend/
|
|||
|
|
git commit -m "feat: 实现报销入口页和票据上传页"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 4.2: 报销草稿页
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `frontend/src/views/DraftView.vue`
|
|||
|
|
- Create: `frontend/src/components/ExpenseTable.vue`
|
|||
|
|
- Create: `frontend/src/api/precheck.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 实现报销草稿页 DraftView**
|
|||
|
|
|
|||
|
|
按文档 9.3 节展示:
|
|||
|
|
- 报销人信息(姓名、部门、成本中心、项目)
|
|||
|
|
- 报销事由
|
|||
|
|
- 费用明细表格(ExpenseTable 组件)
|
|||
|
|
- 票据附件缩略图列表
|
|||
|
|
- AI 自动识别结果标注
|
|||
|
|
- 可编辑字段(金额、事由等可修改)
|
|||
|
|
- 预审状态指示器
|
|||
|
|
- "预审" 按钮 → 跳转到预审结果页
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现 ExpenseTable 组件**
|
|||
|
|
|
|||
|
|
- Ant Design Table 展示费用明细
|
|||
|
|
- 列:费用类型、金额、税额、发生日期、城市、商户、风险等级标签
|
|||
|
|
- 支持行内编辑
|
|||
|
|
- 风险等级彩色标签(绿/黄/橙/红)
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add frontend/
|
|||
|
|
git commit -m "feat: 实现报销草稿页和费用明细表格组件"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 4.3: 预审结果页 + 补件交互页
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `frontend/src/views/PrecheckView.vue`
|
|||
|
|
- Create: `frontend/src/views/SupplementView.vue`
|
|||
|
|
- Create: `frontend/src/components/RuleHitCard.vue`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 实现预审结果页 PrecheckView**
|
|||
|
|
|
|||
|
|
按文档 9.4 节:
|
|||
|
|
- 总体结论卡片(通过/需补件/有风险)
|
|||
|
|
- 风险等级指示(彩色徽章)
|
|||
|
|
- 通过项列表(绿色勾选)
|
|||
|
|
- 风险项列表(RuleHitCard 组件)
|
|||
|
|
- 缺件项列表(橙色提示)
|
|||
|
|
- 每条规则命中显示:问题说明、制度依据、修改建议
|
|||
|
|
- "一键补件" 按钮 → 跳转到补件页
|
|||
|
|
- "确认提交" 按钮(仅预审通过时可用)
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现 RuleHitCard 组件**
|
|||
|
|
|
|||
|
|
- 规则名称和编码
|
|||
|
|
- 风险等级标签
|
|||
|
|
- 问题描述
|
|||
|
|
- 制度依据链接
|
|||
|
|
- 修改建议
|
|||
|
|
- 展开详情
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现补件交互页 SupplementView**
|
|||
|
|
|
|||
|
|
- 显示待补件清单
|
|||
|
|
- 每个补件项:类型(补充附件/补充说明/修改字段)、提示文案
|
|||
|
|
- 上传附件(调用 FileUpload)
|
|||
|
|
- 文本回复输入框
|
|||
|
|
- 提交补件 → 调用 supplement API → 跳转回预审页
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add frontend/
|
|||
|
|
git commit -m "feat: 实现预审结果页和补件交互页"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 4.4: 提交确认页 + 审计日志页
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `frontend/src/views/ConfirmView.vue`
|
|||
|
|
- Create: `frontend/src/views/AuditView.vue`
|
|||
|
|
- Create: `frontend/src/api/audit.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 实现提交确认页 ConfirmView**
|
|||
|
|
|
|||
|
|
- 最终报销单摘要(不可编辑)
|
|||
|
|
- 总金额确认
|
|||
|
|
- 费用明细汇总
|
|||
|
|
- 附件清单
|
|||
|
|
- 同步目标系统选择(MVP 默认 expense_system)
|
|||
|
|
- "确认提交" 按钮 → 调用 submit API
|
|||
|
|
- 同步状态轮询展示(提交中 → 已同步/同步失败)
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 实现审计日志页 AuditView**
|
|||
|
|
|
|||
|
|
按文档 9.5 节(简化版):
|
|||
|
|
- 时间线展示所有操作记录
|
|||
|
|
- 筛选:按任务、操作类型、时间范围
|
|||
|
|
- 每条日志:时间、操作人、操作类型、详情
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add frontend/
|
|||
|
|
git commit -m "feat: 实现提交确认页和审计日志页"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 5: 联调与集成(W5-W6)
|
|||
|
|
|
|||
|
|
### Task 5.1: 前后端联调
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: 多个前后端文件(修复联调问题)
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 启动前后端全栈**
|
|||
|
|
|
|||
|
|
后端:`cd backend && uvicorn app.main:app --reload`
|
|||
|
|
前端:`cd frontend && npm run dev`
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 跑通完整报销流程**
|
|||
|
|
|
|||
|
|
按文档 3.1 节的 10 步流程:
|
|||
|
|
1. 在首页创建报销任务
|
|||
|
|
2. 上传 2-3 张模拟票据(增值税发票、火车票、酒店流水)
|
|||
|
|
3. 点击"开始识别" → Agent 启动
|
|||
|
|
4. 查看草稿页 → 确认自动识别结果
|
|||
|
|
5. 执行预审 → 查看预审结果
|
|||
|
|
6. 如有缺件/风险 → 补件
|
|||
|
|
7. 确认提交 → 查看同步状态
|
|||
|
|
8. 查看审计日志
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 修复联调过程中发现的问题**
|
|||
|
|
|
|||
|
|
API 响应格式不一致、字段缺失、前后端类型不匹配等。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add .
|
|||
|
|
git commit -m "fix: 前后端联调修复"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 5.2: 规则配置与种子数据
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/alembic/seed/expense_policies.sql`
|
|||
|
|
- Create: `backend/alembic/seed/expense_rules.sql`
|
|||
|
|
- Create: `backend/alembic/seed/city_levels.sql`
|
|||
|
|
- Create: `backend/alembic/seed/hotel_limits.sql`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 编写差旅报销制度种子数据**
|
|||
|
|
|
|||
|
|
按典型企业差旅报销制度配置:
|
|||
|
|
- 城市等级(一线/二线/三线)
|
|||
|
|
- 住宿标准(按城市等级 × 职级)
|
|||
|
|
- 交通标准(高铁/飞机按职级)
|
|||
|
|
- 餐补标准
|
|||
|
|
- 必须上传的附件类型映射
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 编写规则种子数据**
|
|||
|
|
|
|||
|
|
至少配置文档 7.2 节的 3 条示例规则 + MVP 需要的 6 条核心规则。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 编写数据初始化脚本**
|
|||
|
|
|
|||
|
|
`backend/scripts/seed_data.py` — 一键初始化所有种子数据。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "feat: 添加差旅报销制度和规则种子数据"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 6: 测试与打磨(W7-W8)
|
|||
|
|
|
|||
|
|
### Task 6.1: 后端集成测试
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `backend/tests/test_integration_flow.py`
|
|||
|
|
- Create: `backend/tests/conftest.py`(更新,添加测试数据库 fixture)
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 编写完整流程集成测试**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_full_reimbursement_flow(db, client):
|
|||
|
|
# 1. 创建任务
|
|||
|
|
task = await create_task(client, user_id="U001", intent="报北京出差费用")
|
|||
|
|
assert task["status"] == "material_collecting"
|
|||
|
|
|
|||
|
|
# 2. 上传票据
|
|||
|
|
doc1 = await upload_document(client, task["task_id"], "vat_invoice", "invoice.pdf")
|
|||
|
|
doc2 = await upload_document(client, task["task_id"], "train_ticket", "train.pdf")
|
|||
|
|
|
|||
|
|
# 3. 启动 Agent
|
|||
|
|
result = await run_agent(client, task["task_id"])
|
|||
|
|
assert result["status"] in ["draft_generated", "prechecking"]
|
|||
|
|
|
|||
|
|
# 4. 获取草稿
|
|||
|
|
draft = await get_draft(client, task["task_id"])
|
|||
|
|
assert len(draft["items"]) > 0
|
|||
|
|
|
|||
|
|
# 5. 获取预审结果
|
|||
|
|
precheck = await get_precheck_result(client, task["task_id"])
|
|||
|
|
assert "risk_level" in precheck
|
|||
|
|
|
|||
|
|
# 6. 如果需要补件
|
|||
|
|
if precheck["precheck_status"] == "need_supplement":
|
|||
|
|
supplements = precheck["rule_hits"]
|
|||
|
|
for s in supplements:
|
|||
|
|
if s["action"] == "require_attachment":
|
|||
|
|
await respond_supplement(client, task["task_id"], s["id"], "已补充")
|
|||
|
|
# 重新预审
|
|||
|
|
await run_agent(client, task["task_id"], start_from="precheck")
|
|||
|
|
|
|||
|
|
# 7. 确认提交
|
|||
|
|
submit = await submit_task(client, task["task_id"])
|
|||
|
|
assert submit["status"] == "submitting"
|
|||
|
|
|
|||
|
|
# 8. 检查同步状态
|
|||
|
|
sync = await get_sync_status(client, task["task_id"])
|
|||
|
|
assert sync["sync_status"] == "success"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 确保所有测试通过**
|
|||
|
|
|
|||
|
|
Run: `cd backend && pytest tests/ -v --tb=short`
|
|||
|
|
Expected: All PASS
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add backend/
|
|||
|
|
git commit -m "test: 添加完整报销流程集成测试"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 6.2: 前端 E2E 测试(可选)
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `frontend/e2e/reimbursement.spec.ts`(如选用 Playwright)
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 安装 Playwright**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd frontend && npm install -D @playwright/test
|
|||
|
|
npx playwright install
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 编写核心流程 E2E 测试**
|
|||
|
|
|
|||
|
|
模拟用户从创建任务到提交确认的完整操作。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 确保测试通过**
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add frontend/
|
|||
|
|
git commit -m "test: 添加前端 E2E 测试"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 6.3: Bug 修复与 UI 打磨
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 走查所有页面,修复视觉和交互问题**
|
|||
|
|
|
|||
|
|
- 响应式布局适配
|
|||
|
|
- Loading 状态
|
|||
|
|
- 错误提示
|
|||
|
|
- 空状态
|
|||
|
|
- 表单校验
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 添加 Demo 数据展示效果**
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add .
|
|||
|
|
git commit -m "fix: UI 打磨和 Bug 修复"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 6.4: 部署与文档
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `docker-compose.prod.yml`
|
|||
|
|
- Modify: `README.md`
|
|||
|
|
- Create: `docs/api.md`
|
|||
|
|
- Create: `docs/deployment.md`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 编写生产 Docker Compose**
|
|||
|
|
|
|||
|
|
包含前后端 + DB + Redis + MinIO + Nginx 反向代理。
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 编写部署文档**
|
|||
|
|
|
|||
|
|
环境要求、配置说明、启动步骤、常用运维命令。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 编写 API 文档**
|
|||
|
|
|
|||
|
|
FastAPI 自动生成 Swagger,补充说明和示例。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 更新 README**
|
|||
|
|
|
|||
|
|
项目简介、架构图、快速启动、开发指南。
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add .
|
|||
|
|
git commit -m "docs: 添加部署文档和 README"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 任务总览
|
|||
|
|
|
|||
|
|
| Phase | 周数 | 任务数 | 可并行 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| Phase 1: 项目基建 | W1 | 4 | 前端骨架 + 后端骨架 + Docker 可并行 |
|
|||
|
|
| Phase 2: 后端核心 | W2-W3 | 6 | 任务API + 文件上传 + OCR 可并行 |
|
|||
|
|
| Phase 3: Agent 编排 | W3-W4 | 4 | Orchestrator → Agents → LLM → 审计(部分串行) |
|
|||
|
|
| Phase 4: 前端页面 | W4-W5 | 4 | 草稿/预审/补件/确认页可并行 |
|
|||
|
|
| Phase 5: 联调集成 | W5-W6 | 2 | 联调 + 种子数据 |
|
|||
|
|
| Phase 6: 测试打磨 | W7-W8 | 4 | 测试 + 修复 + 部署 |
|
|||
|
|
| **总计** | **8 周** | **26 个任务** | |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 风险与缓解
|
|||
|
|
|
|||
|
|
| 风险 | 影响 | 缓解措施 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| OCR 识别准确率不够 | 草稿数据错误 | 允许用户手动修改,低置信度高亮提示 |
|
|||
|
|
| LLM 响应慢或幻觉 | 用户体验差 | Prompt 严格约束输出格式,超时 fallback |
|
|||
|
|
| 规则引擎复杂度超预期 | 延期 | MVP 先做 6 条硬编码规则,JSON 配置化后续迭代 |
|
|||
|
|
| 前后端联调问题多 | 延期 | W5 提前开始联调,边开发边对齐 API |
|
|||
|
|
| 3-5 人不够 | 交付延期 | 优先砍规则管理页和审计页(W8 补) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 验收标准
|
|||
|
|
|
|||
|
|
MVP 完成的标志:
|
|||
|
|
|
|||
|
|
- [x] 用户能通过 Web 界面创建差旅报销任务
|
|||
|
|
- [x] 上传 3 种以上票据类型(增值税发票、火车票、酒店流水)
|
|||
|
|
- [x] OCR 自动识别票据信息并生成报销草稿
|
|||
|
|
- [x] 规则引擎执行 6 条核心预审规则
|
|||
|
|
- [x] 预审结果以可视化方式展示(风险等级、命中规则、修改建议)
|
|||
|
|
- [x] 用户能补件并重新预审
|
|||
|
|
- [x] 用户确认后模拟同步成功
|
|||
|
|
- [x] 影子报销账本完整记录业务数据
|
|||
|
|
- [x] 审计日志记录所有关键操作
|
|||
|
|
- [x] 完整流程端到端测试通过
|