Files
student/student_management_system_complete/student_management_system/main.py
T

138 lines
5.1 KiB
Python
Raw Normal View History

2026-09-21 20:20:16 +08:00
# 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,
)