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