- 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
26 KiB
Phase 1: 项目基建(W1)
目标: 搭建前后端项目骨架、定义数据库模型、配置开发环境,确保团队可以立即开始业务开发。
周期: 第 1 周
任务数: 4 个
可并行: 后端骨架 / 前端骨架 / Docker Compose 可完全并行
前置依赖: 无
本阶段交付物
| 交付物 | 说明 |
|---|---|
| 后端项目骨架 | FastAPI + SQLAlchemy + Alembic,可运行的健康检查 |
| 数据库 Schema | 全部 12 张表的 ORM 模型 + Alembic 迁移 |
| 前端项目骨架 | Vue3 + TS + Ant Design Vue,路由和 API 层配置 |
| 开发环境 | Docker Compose(PostgreSQL + 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 # 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:
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已创建,配置项说明完整