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

26 KiB
Raw Blame History

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

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:

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:

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:

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:

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

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:

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:

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

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

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

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:

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 导出所有模型
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
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 项目

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:

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:

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

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
# 数据库
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
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
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 已创建,配置项说明完整