Files
ToDoList/api/app/main.py
祀梦 2979197b1c release: Elysia ToDo v1.0.0
鍏ㄦ爤涓汉淇℃伅绠$悊搴旂敤锛岄泦鎴愬緟鍔炰换鍔°€佷範鎯墦鍗°€佺邯蹇垫棩鎻愰啋銆佽祫浜ф€昏鍔熻兘銆

Made-with: Cursor
2026-03-14 22:21:26 +08:00

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)