138 lines
5.1 KiB
Python
138 lines
5.1 KiB
Python
# main.py
|
||||
|
|
# 项目入口:创建 FastAPI 应用、注册中间件与路由、注册全局异常处理、托管前端页面、启动服务。
|
|||
|
|
#
|
|||
|
|
# 原版的几处硬伤:
|
|||
|
|
# 1. allow_origins=["*"] + allow_credentials=True —— 浏览器规范禁止这个组合,
|
|||
|
|
# 带 Cookie 的请求一定会失败。已改为显式白名单。
|
|||
|
|
# 2. if __name__ == "__main__" 里 init_all(True, False) —— "启动服务"
|
|||
|
|
# 和"建表 + 灌种子数据"耦合在一起,线上重启一次就可能动到数据。
|
|||
|
|
# 改为独立的 init_db.py 脚本 + lifespan 里只做健康检查。
|
|||
|
|
# 3. 没有统一异常处理,任何未捕获异常都以 500 + 堆栈露出。
|
|||
|
|
# 4. 没有日志配置,出问题只能靠 print。
|
|||
|
|
# 5. 【本次新增】只跑 main.py 时看不到前端界面,只有一个 Swagger 文档。
|
|||
|
|
# 现在把 frontend/ 目录挂到根路径上,启动后端 = 整套系统可用,
|
|||
|
|
# 而且还顺带消灭了跨域问题(同源就不需要 CORS)。
|
|||
|
|
|
|||
|
|
import logging
|
|||
|
|
from contextlib import asynccontextmanager
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
from fastapi import FastAPI
|
|||
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|||
|
|
from fastapi.staticfiles import StaticFiles
|
|||
|
|
from starlette.middleware.gzip import GZipMiddleware
|
|||
|
|
|
|||
|
|
from settings import settings
|
|||
|
|
|
|||
|
|
logging.basicConfig(
|
|||
|
|
level=logging.DEBUG if settings.DEBUG else logging.INFO,
|
|||
|
|
format="%(asctime)s | %(levelname)-7s | %(name)s | %(message)s",
|
|||
|
|
)
|
|||
|
|
logger = logging.getLogger("app")
|
|||
|
|
|
|||
|
|
# 前端目录:与本文件同级目录的上一级下的 frontend/
|
|||
|
|
# 用 __file__ 推导而不是写死路径,项目整个拷走也不会失效。
|
|||
|
|
FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@asynccontextmanager
|
|||
|
|
async def lifespan(app: FastAPI):
|
|||
|
|
logger.info("服务启动:%s v%s,API 前缀 %s", settings.APP_NAME, settings.APP_VERSION, settings.API_PREFIX)
|
|||
|
|
yield
|
|||
|
|
logger.info("服务关闭")
|
|||
|
|
|
|||
|
|
|
|||
|
|
app = FastAPI(
|
|||
|
|
title=settings.APP_NAME,
|
|||
|
|
description="学生管理系统 —— FastAPI + SQLAlchemy 分层架构(MySQL)",
|
|||
|
|
version=settings.APP_VERSION,
|
|||
|
|
lifespan=lifespan,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---------------- 中间件 ----------------
|
|||
|
|
# 生产环境请把 CORS_ORIGINS 收窄到真实前端域名
|
|||
|
|
app.add_middleware(
|
|||
|
|
CORSMiddleware,
|
|||
|
|
allow_origins=settings.CORS_ORIGINS,
|
|||
|
|
allow_credentials=True,
|
|||
|
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|||
|
|
allow_headers=["*"],
|
|||
|
|
)
|
|||
|
|
# 响应体压缩:列表接口返回几百条 JSON 时能省 60%+ 流量
|
|||
|
|
app.add_middleware(GZipMiddleware, minimum_size=1024)
|
|||
|
|
|
|||
|
|
# ---------------- 全局异常处理 ----------------
|
|||
|
|
from exceptions import register_exception_handlers # noqa: E402
|
|||
|
|
|
|||
|
|
register_exception_handlers(app)
|
|||
|
|
|
|||
|
|
# ---------------- 路由 ----------------
|
|||
|
|
from api.router import api_router # noqa: E402
|
|||
|
|
|
|||
|
|
app.include_router(api_router, prefix=settings.API_PREFIX)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------- 基础端点 ----------------
|
|||
|
|
@app.get("/health", tags=["系统"], summary="健康检查")
|
|||
|
|
async def health():
|
|||
|
|
"""健康检查要真的探一下数据库。
|
|||
|
|
|
|||
|
|
只返回 {"status":"ok"} 的假健康检查在真实运维里是负资产 ——
|
|||
|
|
容器编排会认为服务正常,而实际上数据库早就挂了。
|
|||
|
|
"""
|
|||
|
|
from sqlalchemy import text
|
|||
|
|
|
|||
|
|
from database import engine
|
|||
|
|
|
|||
|
|
db_ok, db_error = True, None
|
|||
|
|
try:
|
|||
|
|
with engine.connect() as conn:
|
|||
|
|
conn.execute(text("SELECT 1"))
|
|||
|
|
except Exception as exc: # noqa: BLE001
|
|||
|
|
db_ok, db_error = False, str(exc)
|
|||
|
|
logger.error("健康检查:数据库不可达 -> %s", exc)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"status": "ok" if db_ok else "degraded",
|
|||
|
|
"app": settings.APP_NAME,
|
|||
|
|
"version": settings.APP_VERSION,
|
|||
|
|
"database": "up" if db_ok else "down",
|
|||
|
|
"api_prefix": settings.API_PREFIX,
|
|||
|
|
"frontend": "/" if FRONTEND_DIR.is_dir() else None,
|
|||
|
|
"docs": "/docs",
|
|||
|
|
"detail": db_error,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------- 前端托管 ----------------
|
|||
|
|
# 放在最后注册:Starlette 按注册顺序匹配路由,Mount("/") 是个通吃规则,
|
|||
|
|
# 必须排在 /api/v1/**、/docs、/health 后面,否则会把接口全挡掉。
|
|||
|
|
if FRONTEND_DIR.is_dir():
|
|||
|
|
app.mount("/", StaticFiles(directory=str(FRONTEND_DIR), html=True), name="frontend")
|
|||
|
|
logger.info("已托管前端页面:http://%s:%s/ <- %s", settings.HOST, settings.PORT, FRONTEND_DIR)
|
|||
|
|
else:
|
|||
|
|
# 只部署后端(没带 frontend 目录)时不报错,只是提供一份说明
|
|||
|
|
@app.get("/", tags=["系统"], summary="服务信息")
|
|||
|
|
async def root():
|
|||
|
|
return {
|
|||
|
|
"message": f"{settings.APP_NAME} 服务运行中(未找到 frontend 目录,仅提供 API)",
|
|||
|
|
"version": settings.APP_VERSION,
|
|||
|
|
"docs": "/docs",
|
|||
|
|
"health": "/health",
|
|||
|
|
"api_prefix": settings.API_PREFIX,
|
|||
|
|
}
|
|||
|
|
logger.warning("未找到前端目录 %s,只提供 API。看界面请启动 frontend 下的静态服务。", FRONTEND_DIR)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
import uvicorn
|
|||
|
|
|
|||
|
|
# 只负责启动。建表 / 灌数据请执行:
|
|||
|
|
# python init_db.py
|
|||
|
|
uvicorn.run(
|
|||
|
|
"main:app",
|
|||
|
|
host=settings.HOST,
|
|||
|
|
port=settings.PORT,
|
|||
|
|
reload=settings.DEBUG,
|
|||
|
|
)
|