145 lines
4.1 KiB
Python
145 lines
4.1 KiB
Python
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)
|