| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- from fastapi import FastAPI, Request, HTTPException
- from contextlib import asynccontextmanager
- from fastapi.middleware.cors import CORSMiddleware
- from fastapi.responses import JSONResponse, FileResponse
- from fastapi.staticfiles import StaticFiles
- import os
- from app.core.config import settings
- from app.api.v1.api import api_router
- from app.db.session import db_manager
- from app.core.responses.base import Response
- from fastapi.exceptions import RequestValidationError
- import logging
- import uuid
- # Configure logging
- logging.basicConfig(
- level=getattr(logging, os.environ.get("LOG_LEVEL", "INFO").upper(), logging.INFO),
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
- )
- logger = logging.getLogger(__name__)
- settings.validate_production_security()
- # Initialize Database Schema
- try:
- db_manager.init_db()
- except Exception as e:
- logger.error(f"Database initialization failed: {e}", exc_info=True)
- # Continue to allow app to start and report error via API
- @asynccontextmanager
- async def lifespan(app: FastAPI):
- from app.services.forum_scheduler import scheduler
- recovered = await scheduler.recover_running_forums()
- if recovered:
- logger.info("Recovered running forums: %s", recovered)
- try:
- yield
- finally:
- await scheduler.shutdown()
- app = FastAPI(
- title=settings.PROJECT_NAME,
- openapi_url=f"{settings.API_V1_STR}/openapi.json",
- lifespan=lifespan,
- )
- # Global Exception Handler
- @app.exception_handler(Exception)
- async def global_exception_handler(request: Request, exc: Exception):
- error_id = str(uuid.uuid4())
- logger.exception("Unhandled request error [%s]", error_id)
-
- # Return structured error response
- return JSONResponse(
- status_code=500,
- content={
- "code": 500,
- "detail": "internal server error",
- "error_id": error_id,
- "message": "服务器内部错误,请稍后重试",
- "data": None
- },
- )
- @app.exception_handler(HTTPException)
- async def http_exception_handler(request: Request, exc: HTTPException):
- return JSONResponse(
- status_code=exc.status_code,
- content={
- "code": exc.status_code,
- "detail": exc.detail,
- "message": exc.detail,
- "data": None
- },
- )
- @app.exception_handler(RequestValidationError)
- async def validation_exception_handler(request: Request, exc: RequestValidationError):
- errors = exc.errors()
- logger.warning(f"Validation error: {errors}")
- serializable_errors = []
- for error in errors:
- item = dict(error)
- context = item.get("ctx")
- if context:
- item["ctx"] = {key: str(value) for key, value in context.items()}
- serializable_errors.append(item)
- return JSONResponse(
- status_code=400,
- content={
- "code": 400,
- "detail": serializable_errors,
- "message": "请求参数验证失败",
- "data": None
- },
- )
- # Set all CORS enabled origins
- app.add_middleware(
- CORSMiddleware,
- allow_origins=settings.cors_origins,
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
- )
- app.include_router(api_router, prefix=settings.API_V1_STR)
- # Serve Frontend Static Files
- # In Docker/Production, we build the frontend and put it in /app/frontend/dist (as per Dockerfile)
- # Or ./frontend/dist relative to app root?
- # Dockerfile copies frontend/dist to /app/frontend/dist
- # But WORKDIR is /app
- # So path is ./frontend/dist
- # Let's be robust
- base_dir = os.path.dirname(os.path.abspath(__file__)) # /app/app
- root_dir = os.path.dirname(base_dir) # /app
- frontend_dist = os.path.join(root_dir, "frontend", "dist")
- if not os.path.exists(frontend_dist):
- # Try alternate location if running locally not in docker
- frontend_dist = os.path.join(root_dir, "..", "frontend", "dist")
- logger.info(f"Frontend dist path: {frontend_dist}, exists: {os.path.exists(frontend_dist)}")
- if os.path.exists(frontend_dist):
- app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dist, "assets")), name="assets")
-
- # Catch-all for SPA routing
- @app.get("/{full_path:path}")
- async def serve_spa(full_path: str):
- # API requests are handled by router above (order matters? No, this is catch-all)
- # But include_router is already added.
- if full_path.startswith("api"):
- return JSONResponse(status_code=404, content={"detail": "API endpoint not found"})
-
- # Check if file exists (e.g. favicon.ico)
- file_path = os.path.join(frontend_dist, full_path)
- if os.path.exists(file_path) and os.path.isfile(file_path):
- return FileResponse(file_path)
-
- # Fallback to index.html for client-side routing
- index_path = os.path.join(frontend_dist, "index.html")
- if os.path.exists(index_path):
- return FileResponse(index_path)
-
- return JSONResponse(status_code=404, content={"detail": "Not Found"})
- @app.get("/")
- def root():
- index_path = os.path.join(frontend_dist, "index.html")
- if os.path.exists(index_path):
- return FileResponse(index_path)
- return {"message": "Welcome to MADF API. Frontend not found.", "docs": "/docs"}
- if __name__ == "__main__":
- import uvicorn
- uvicorn.run(app, host="0.0.0.0", port=8000)
|