Implement Phase 1 backend: FastAPI upload API with MinIO and PostgreSQL

- Add FastAPI app with CORS and auth middleware (Bearer token)
- Add POST /api/v1/transactions/upload endpoint (multipart/form-data)
- Add MinIO storage service with date-partitioned object keys
- Add PostgreSQL database service for transaction metadata
- Add Dockerfile and docker-compose.yml for deployment
- Add config.example.yaml template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-05-21 16:42:08 +08:00
parent a7ae5cba5d
commit e3c84540eb
16 changed files with 306 additions and 0 deletions

61
app/routers/upload.py Normal file
View File

@@ -0,0 +1,61 @@
import logging
from fastapi import APIRouter, File, Form, UploadFile
from fastapi.responses import JSONResponse
from app.services import database, minio
router = APIRouter(tags=["upload"])
logger = logging.getLogger(__name__)
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
@router.post("/transactions/upload")
async def upload_transaction(
file: UploadFile = File(...),
original_filename: str | None = Form(None),
user_note: str | None = Form(None),
):
# Read file content
content = await file.read()
if len(content) == 0:
return JSONResponse(
status_code=422,
content={"success": False, "error": "Missing required field: file"},
)
if len(content) > MAX_FILE_SIZE:
return JSONResponse(
status_code=413,
content={"success": False, "error": "File size exceeds 10MB limit"},
)
filename = original_filename or file.filename or "upload.jpg"
# Upload to MinIO
try:
object_key = minio.upload_image(content, filename)
except Exception:
logger.exception("Failed to upload image to storage")
return JSONResponse(
status_code=503,
content={"success": False, "error": "Storage service unavailable"},
)
# Insert metadata into PostgreSQL
try:
result = database.insert_transaction(
image_object_key=object_key,
original_filename=filename,
user_note=user_note if user_note else None,
)
except Exception:
logger.exception("Failed to save transaction metadata")
return JSONResponse(
status_code=500,
content={"success": False, "error": "Failed to save transaction metadata"},
)
return {"success": True, "data": result}