from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, FileResponse import os import time import json from app.config import CORS_ORIGINS, WEBUI_PATH, HOST, PORT from app.database import init_db from app.routers import api_router from app.utils.logger import logger @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期管理""" # 启动时 logger.info("应用启动中...") init_db() logger.info("数据库初始化完成") yield # 关闭时 logger.info("应用关闭中...") # 创建 FastAPI 应用 app = FastAPI( title="爱莉希雅待办事项 API", description="Elysia ToDo - 个人信息管理应用", version="1.0.0", lifespan=lifespan, ) # 配置 CORS app.add_middleware( CORSMiddleware, allow_origins=CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 请求日志中间件 @app.middleware("http") async def log_requests(request: Request, call_next): start_time = time.time() # 记录请求信息 request_method = request.method request_path = request.url.path query_params = dict(request.query_params) if request.query_params else None # 构建日志信息 log_parts = [f"请求开始 -> {request_method} {request_path}"] if query_params: log_parts.append(f"Query参数: {json.dumps(query_params, ensure_ascii=False)}") # 尝试读取请求体(仅对有 body 的方法) body_info = None if request.method in ["POST", "PUT", "PATCH"]: try: body_bytes = await request.body() if body_bytes: try: body_json = json.loads(body_bytes) body_info = json.dumps(body_json, ensure_ascii=False) except json.JSONDecodeError: body_info = body_bytes.decode('utf-8', errors='replace')[:200] log_parts.append(f"Body: {body_info}") except Exception: pass logger.info(" | ".join(log_parts)) # 执行请求 response = await call_next(request) # 计算耗时 process_time = (time.time() - start_time) * 1000 # 记录响应信息 logger.info( f"请求完成 <- {request_method} {request_path} | " f"状态码: {response.status_code} | 耗时: {process_time:.2f}ms" ) return response # 全局异常处理器 @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): # 使用 exc_info=True 记录完整堆栈信息 logger.error( f"全局异常: {request.method} {request.url.path} | 错误: {str(exc)}", exc_info=True ) return JSONResponse( status_code=500, content={ "success": False, "message": "服务器内部错误", "error_code": "INTERNAL_ERROR" } ) # 注册路由 app.include_router(api_router) # 健康检查(必须在 static mount 之前注册,否则会被静态文件拦截) @app.get("/health") async def health_check(): """健康检查""" return {"status": "ok", "message": "服务运行正常"} # SPA 静态文件回退路由(支持前端 History 模式路由) if os.path.exists(WEBUI_PATH): @app.get("/") async def serve_index(): return FileResponse(os.path.join(WEBUI_PATH, "index.html")) @app.get("/{full_path:path}") async def spa_fallback(request: Request, full_path: str): """SPA 回退:先尝试提供真实文件,找不到则返回 index.html""" file_path = os.path.join(WEBUI_PATH, full_path) if os.path.isfile(file_path): return FileResponse(file_path) return FileResponse(os.path.join(WEBUI_PATH, "index.html")) logger.info(f"SPA 静态文件服务已配置: {WEBUI_PATH}") else: logger.warning(f"WebUI 目录不存在: {WEBUI_PATH}") if __name__ == "__main__": import uvicorn logger.info(f"启动服务: {HOST}:{PORT}") uvicorn.run(app, host=HOST, port=PORT)