"""
FastAPI应用主入口
"""
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from loguru import logger
import sys
from app.core.config import settings
from app.core.exceptions import BusinessException
from app.core.response import error_response
from app.middleware.api_transform import api_transform_middleware
from app.api.v1 import api_router
from app.db.session import init_db, close_db
# 配置日志
logger.remove()
logger.add(
sys.stderr,
level=settings.LOG_LEVEL,
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
colorize=True
)
logger.add(
settings.LOG_FILE,
rotation=settings.LOG_ROTATION,
retention=settings.LOG_RETENTION,
level=settings.LOG_LEVEL,
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
encoding="utf-8"
)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
# 启动时执行
logger.info("🚀 应用启动中...")
logger.info(f"📦 环境: {settings.APP_ENVIRONMENT}")
logger.info(f"🔗 数据库: {settings.DATABASE_URL}")
# 初始化数据库(生产环境使用Alembic迁移)
if settings.is_development:
await init_db()
logger.info("✅ 数据库初始化完成")
yield
# 关闭时执行
logger.info("🛑 应用关闭中...")
await close_db()
logger.info("✅ 数据库连接已关闭")
# 创建FastAPI应用
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
description="企业级资产管理系统后端API",
docs_url="/docs" if settings.DEBUG else None,
redoc_url="/redoc" if settings.DEBUG else None,
openapi_url="/openapi.json" if settings.DEBUG else None,
lifespan=lifespan
)
# 配置CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=settings.CORS_ALLOW_CREDENTIALS,
allow_methods=settings.CORS_ALLOW_METHODS,
allow_headers=settings.CORS_ALLOW_HEADERS,
)
# API request/response normalization
app.middleware("http")(api_transform_middleware)
# 自定义异常处理器
@app.exception_handler(BusinessException)
async def business_exception_handler(request: Request, exc: BusinessException):
"""业务异常处理"""
logger.warning(f"业务异常: {exc.message} - 错误码: {exc.error_code}")
return JSONResponse(
status_code=exc.code,
content=error_response(
code=exc.code,
message=exc.message,
errors=[{"field": k, "message": v} for k, v in exc.data.items()] if exc.data else None
)
)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
"""HTTP异常处理"""
logger.warning(f"HTTP异常: {exc.status_code} - {exc.detail}")
return JSONResponse(
status_code=exc.status_code,
content=error_response(
code=exc.status_code,
message=str(exc.detail)
)
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""请求验证异常处理"""
errors = []
for error in exc.errors():
errors.append({
"field": ".".join(str(loc) for loc in error["loc"]),
"message": error["msg"]
})
logger.warning(f"验证异常: {errors}")
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=error_response(
code=status.HTTP_422_UNPROCESSABLE_ENTITY,
message="参数验证失败",
errors=errors
)
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
"""通用异常处理"""
logger.error(f"未处理的异常: {type(exc).__name__} - {str(exc)}", exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=error_response(
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
message="服务器内部错误" if not settings.DEBUG else str(exc)
)
)
# 注册路由
app.include_router(api_router, prefix=settings.API_V1_PREFIX)
# 健康检查
@app.get("/health", tags=["系统"])
async def health_check():
"""健康检查接口"""
return {
"status": "ok",
"app_name": settings.APP_NAME,
"version": settings.APP_VERSION,
"environment": settings.APP_ENVIRONMENT
}
# 根路径
@app.get("/", tags=["系统"])
async def root():
"""根路径"""
return {
"message": f"欢迎使用{settings.APP_NAME} API",
"version": settings.APP_VERSION,
"docs": "/docs" if settings.DEBUG else None
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host=settings.HOST,
port=settings.PORT,
reload=settings.DEBUG,
log_level=settings.LOG_LEVEL.lower()
)