Files
X-Financial/docs/plans/phase-1-project-infra/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

803 lines
26 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 1: 项目基建W1
> **目标:** 搭建前后端项目骨架、定义数据库模型、配置开发环境,确保团队可以立即开始业务开发。
> **周期:** 第 1 周
> **任务数:** 4 个
> **可并行:** 后端骨架 / 前端骨架 / Docker Compose 可完全并行
> **前置依赖:** 无
---
## 本阶段交付物
| 交付物 | 说明 |
|---|---|
| 后端项目骨架 | FastAPI + SQLAlchemy + Alembic可运行的健康检查 |
| 数据库 Schema | 全部 12 张表的 ORM 模型 + Alembic 迁移 |
| 前端项目骨架 | Vue3 + TS + Ant Design Vue路由和 API 层配置 |
| 开发环境 | Docker ComposePostgreSQL + Redis + MinIO |
---
## 任务清单
### Task 1.1: 后端项目骨架搭建
**负责人:** 后端工程师 A
**预计工时:** 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 # SettingsPydantic 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
```
- [ ] **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 + 迁移
**负责人:** 后端工程师 A
**预计工时:** 1.5 天
**前置依赖:** Task 1.1
**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: 定义 Base Mixin**
`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)
```
- [ ] **Step 3: 定义 ReimbursementTask 模型**
`backend/app/models/task.py`:
```python
from sqlalchemy import String, Text
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)
documents = relationship("ExpenseDocument", back_populates="task", lazy="selectin")
reimbursement = relationship("ShadowReimbursement", back_populates="task", uselist=False, lazy="selectin")
```
- [ ] **Step 4: 定义 ShadowReimbursement + ReimbursementItem + SupplementRequest + SyncRecord**
`backend/app/models/reimbursement.py` — 字段按开发文档 5.2.2 ~ 5.2.4, 5.2.7, 5.2.8 节:
```python
from sqlalchemy import String, Text, Numeric, Date, ForeignKey, Enum as SAEnum
from sqlalchemy.orm import Mapped, mapped_column, relationship
from decimal import Decimal
from datetime import date
from app.models.base import IDMixin, TimestampMixin, Base
from app.models.enums import RiskLevel, SyncStatus
class ShadowReimbursement(IDMixin, TimestampMixin, Base):
__tablename__ = "shadow_reimbursement"
task_id: Mapped[str] = mapped_column(String(36), ForeignKey("reimbursement_task.id"), unique=True)
applicant_id: Mapped[str] = mapped_column(String(36))
department_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
cost_center_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
project_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
reimbursement_type: Mapped[str] = mapped_column(String(50))
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
total_amount: Mapped[Decimal] = mapped_column(Numeric(12, 2), default=Decimal("0"))
currency: Mapped[str] = mapped_column(String(10), default="CNY")
risk_level: Mapped[str | None] = mapped_column(String(20), nullable=True)
precheck_status: Mapped[str | None] = mapped_column(String(30), nullable=True)
backend_system: Mapped[str | None] = mapped_column(String(50), nullable=True)
backend_bill_id: Mapped[str | None] = mapped_column(String(50), nullable=True)
sync_status: Mapped[str | None] = mapped_column(String(20), nullable=True)
task = relationship("ReimbursementTask", back_populates="reimbursement")
items = relationship("ReimbursementItem", back_populates="reimbursement", lazy="selectin")
class ReimbursementItem(IDMixin, TimestampMixin, Base):
__tablename__ = "reimbursement_item"
reimbursement_id: Mapped[str] = mapped_column(String(36), ForeignKey("shadow_reimbursement.id"))
expense_type: Mapped[str] = mapped_column(String(50))
amount: Mapped[Decimal] = mapped_column(Numeric(12, 2))
tax_amount: Mapped[Decimal | None] = mapped_column(Numeric(12, 2), nullable=True)
occurred_at: Mapped[date | None] = mapped_column(Date, nullable=True)
city: Mapped[str | None] = mapped_column(String(50), nullable=True)
vendor_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
invoice_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
policy_status: Mapped[str | None] = mapped_column(String(20), nullable=True)
risk_level: Mapped[str | None] = mapped_column(String(20), nullable=True)
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
reimbursement = relationship("ShadowReimbursement", back_populates="items")
class SupplementRequest(IDMixin, TimestampMixin, Base):
__tablename__ = "supplement_request"
reimbursement_id: Mapped[str] = mapped_column(String(36), ForeignKey("shadow_reimbursement.id"))
request_type: Mapped[str] = mapped_column(String(30)) # attachment / explanation / field_modify
target_item_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
message: Mapped[str] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(20), default="pending") # pending / resolved / closed
user_response: Mapped[str | None] = mapped_column(Text, nullable=True)
resolved_at: Mapped[date | None] = mapped_column(Date, nullable=True)
class SyncRecord(IDMixin, TimestampMixin, Base):
__tablename__ = "sync_record"
reimbursement_id: Mapped[str] = mapped_column(String(36), ForeignKey("shadow_reimbursement.id"))
target_system: Mapped[str] = mapped_column(String(50))
request_payload: Mapped[dict | None] = mapped_column(Text, nullable=True) # JSON string
response_payload: Mapped[dict | None] = mapped_column(Text, nullable=True) # JSON string
sync_status: Mapped[str] = mapped_column(String(20), default="pending")
backend_bill_id: Mapped[str | None] = mapped_column(String(50), nullable=True)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
```
- [ ] **Step 5: 定义 ExpenseDocument 模型**
`backend/app/models/document.py` — 字段按开发文档 5.2.4 节:
```python
from sqlalchemy import String, Text, Numeric, Date, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from decimal import Decimal
from datetime import date
from app.models.base import IDMixin, TimestampMixin, Base
class ExpenseDocument(IDMixin, TimestampMixin, Base):
__tablename__ = "expense_document"
reimbursement_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("shadow_reimbursement.id"), nullable=True)
item_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
task_id: Mapped[str] = mapped_column(String(36), ForeignKey("reimbursement_task.id"))
document_type: Mapped[str] = mapped_column(String(30))
file_url: Mapped[str] = mapped_column(String(500))
ocr_status: Mapped[str] = mapped_column(String(20), default="pending") # pending / processing / done / failed
extracted_json: Mapped[dict | None] = mapped_column(Text, nullable=True)
invoice_code: Mapped[str | None] = mapped_column(String(30), nullable=True)
invoice_number: Mapped[str | None] = mapped_column(String(30), nullable=True)
invoice_date: Mapped[date | None] = mapped_column(Date, nullable=True)
amount: Mapped[Decimal | None] = mapped_column(Numeric(12, 2), nullable=True)
tax_amount: Mapped[Decimal | None] = mapped_column(Numeric(12, 2), nullable=True)
seller_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
buyer_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
verify_status: Mapped[str | None] = mapped_column(String(20), nullable=True)
duplicate_status: Mapped[str | None] = mapped_column(String(20), nullable=True)
task = relationship("ReimbursementTask", back_populates="documents")
```
- [ ] **Step 6: 定义 ExpenseRule + RuleHit 模型**
`backend/app/models/rule.py` — 字段按开发文档 5.2.5, 5.2.6 节:
```python
from sqlalchemy import String, Text, Boolean
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import IDMixin, TimestampMixin, Base
class ExpenseRule(IDMixin, TimestampMixin, Base):
__tablename__ = "expense_rule"
rule_code: Mapped[str] = mapped_column(String(50), unique=True)
rule_name: Mapped[str] = mapped_column(String(100))
expense_type: Mapped[str] = mapped_column(String(50))
condition_json: Mapped[dict] = mapped_column(Text) # JSON string
action: Mapped[str] = mapped_column(String(30))
severity: Mapped[str] = mapped_column(String(20))
message_template: Mapped[str] = mapped_column(Text)
policy_ref: Mapped[str | None] = mapped_column(String(200), nullable=True)
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
version: Mapped[str] = mapped_column(String(10), default="1.0")
class RuleHit(IDMixin, TimestampMixin, Base):
__tablename__ = "rule_hit"
reimbursement_id: Mapped[str] = mapped_column(String(36))
item_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
rule_id: Mapped[str] = mapped_column(String(36))
severity: Mapped[str] = mapped_column(String(20))
hit_result: Mapped[dict | None] = mapped_column(Text, nullable=True) # JSON string
explanation: Mapped[str | None] = mapped_column(Text, nullable=True)
suggestion: Mapped[str | None] = mapped_column(Text, nullable=True)
```
- [ ] **Step 7: 定义 AuditLog 模型**
`backend/app/models/audit.py`:
```python
from sqlalchemy import String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import IDMixin, TimestampMixin, Base
class AuditLog(IDMixin, TimestampMixin, Base):
__tablename__ = "audit_log"
action: Mapped[str] = mapped_column(String(50), index=True) # upload / ocr / agent / rule_hit / supplement / confirm / sync
actor: Mapped[str] = mapped_column(String(36), index=True)
target_type: Mapped[str] = mapped_column(String(50)) # task / document / reimbursement / rule
target_id: Mapped[str] = mapped_column(String(36))
detail: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON string
```
- [ ] **Step 8: 更新 `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 9: 生成 Alembic 迁移**
Run: `cd backend && alembic revision --autogenerate -m "init schema"`
Run: `cd backend && alembic upgrade head`
- [ ] **Step 10: 编写模型测试**
`backend/tests/test_models.py` — 验证所有表能正确创建和插入数据。
- [ ] **Step 11: Commit**
```bash
git add backend/
git commit -m "feat: 完成所有数据模型定义和数据库迁移"
```
---
### Task 1.3: 前端项目骨架搭建
**负责人:** 前端工程师
**预计工时:** 1 天
**前置依赖:** 无(可与 Task 1.1 并行)
**Files:**
- Create: Vue3 + TypeScript 项目Vite 初始化)
- Create: `frontend/src/router/index.ts`
- Create: `frontend/src/stores/`
- Create: `frontend/src/api/`
- Create: `frontend/src/views/`
- Create: `frontend/src/components/`
- Create: `frontend/src/layouts/`
- [ ] **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 开发环境
**负责人:** 后端工程师 B
**预计工时:** 0.5 天
**前置依赖:** 无(可与 Task 1.1、1.3 并行)
**Files:**
- Create: `docker-compose.yml`
- Create: `backend/Dockerfile`
- Create: `frontend/Dockerfile`
- Create: `.env.example`
- [ ] **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**
```env
# 数据库
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/x_financial
# Redis
REDIS_URL=redis://localhost:6379/0
# MinIO
MINIO_ENDPOINT=localhost:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=reimbursement
# OCR必填正式环境开发环境可用 mock
OCR_PROVIDER=mock
BAIDU_OCR_API_KEY=
BAIDU_OCR_SECRET_KEY=
# LLM必填正式环境
LLM_PROVIDER=openai
LLM_API_KEY=
LLM_MODEL=gpt-4o-mini
LLM_BASE_URL=
# 前端
VITE_API_BASE_URL=http://localhost:8000/api/v1
```
- [ ] **Step 3: 编写后端 Dockerfile**
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
- [ ] **Step 4: 验证环境启动**
Run: `docker-compose up -d`
Run: `docker-compose ps`
Expected: postgres, redis, minio 均为 running
- [ ] **Step 5: Commit**
```bash
git add docker-compose.yml .env.example backend/Dockerfile frontend/Dockerfile
git commit -m "feat: 添加 Docker Compose 开发环境PostgreSQL + Redis + MinIO"
```
---
## 本阶段完成检查
- [ ] `cd backend && pytest tests/test_health.py -v` 通过
- [ ] `cd backend && alembic upgrade head` 无报错,所有表已创建
- [ ] `cd frontend && npm run dev` 能正常启动
- [ ] `docker-compose up -d` 三个服务均 running
- [ ] `.env.example` 已创建,配置项说明完整