Files

168 lines
5.8 KiB
Python
Raw Permalink Normal View History

2026-09-21 19:03:31 +08:00
"""应用入口。
启动方式:
uvicorn app.main:app --reload # 开发
python -m app.main # 等价写法(会先自动建表)
接口文档:
http://127.0.0.1:8000/docs # Swagger UI
http://127.0.0.1:8000/redoc # ReDoc
http://127.0.0.1:8000/ # 前端页面
"""
from __future__ import annotations
import logging
import sys
import time
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
# --- 让「直接运行本文件」也能工作 -------------------------------------------
# `python app/main.py`(或 PyCharm 里右键 Run)时,Python 把 sys.path[0]
# 设成**脚本所在目录** app/,于是下面那行 `from app.api import ...`
# 找不到 app 包,报 ModuleNotFoundError: No module named 'app'。
# 把项目根补进 sys.path,这条路才通。
# 用 `python -m app.main` / `uvicorn app.main:app` / 项目根的 run.py 启动时,
# 项目根本来就在 sys.path 里,这几行是无害的。
_ROOT = str(Path(__file__).resolve().parent.parent)
if _ROOT not in sys.path:
sys.path.insert(0, _ROOT)
# ---------------------------------------------------------------------------
from app.api import api_router
from app.core.config import settings
from app.core.database import engine, ensure_database_exists
from app.core.exceptions import BusinessError
from app.model import Base
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-7s %(name)s %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("wolin")
STATIC_DIR = Path(__file__).parent / "static"
@asynccontextmanager
async def lifespan(app: FastAPI):
ensure_database_exists()
Base.metadata.create_all(bind=engine)
logger.info("数据库就绪:%s", settings.DB_NAME)
logger.info("接口文档:http://127.0.0.1:8000/docs")
yield
engine.dispose()
app = FastAPI(
title=settings.APP_NAME,
description=(
f"{settings.APP_DESC}\n\n"
"**统一响应格式**:`{code, msg, data}`,`code=0` 表示成功。\n\n"
"**鉴权**:除 `/auth/login` 外都需要在请求头带 `Authorization: Bearer <token>`。\n\n"
"**分层**:api(路由)/ service(业务)/ dao(数据访问)/ model(ORM)/ schema(校验)。"
),
version=settings.VERSION,
lifespan=lifespan,
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ==================================================================== 异常处理
@app.exception_handler(BusinessError)
async def business_error_handler(request: Request, exc: BusinessError):
return JSONResponse(status_code=200, content={"code": exc.code, "msg": exc.msg, "data": exc.data})
@app.exception_handler(RequestValidationError)
async def validation_error_handler(request: Request, exc: RequestValidationError):
"""把 Pydantic 的报错翻译成人话,前端可以直接弹。"""
problems = []
for error in exc.errors():
location = " → ".join(str(x) for x in error.get("loc", []) if x != "body")
problems.append(f"{location}: {error.get('msg')}")
return JSONResponse(
status_code=200,
content={"code": 422, "msg": "参数校验未通过:" + ";".join(problems), "data": {"errors": problems}},
)
@app.exception_handler(IntegrityError)
async def integrity_error_handler(request: Request, exc: IntegrityError):
logger.warning("数据库约束冲突:%s", exc)
return JSONResponse(status_code=200, content={"code": 409, "msg": "数据冲突(唯一键重复或外键不合法)", "data": None})
@app.exception_handler(SQLAlchemyError)
async def sqlalchemy_error_handler(request: Request, exc: SQLAlchemyError):
logger.exception("数据库错误")
return JSONResponse(status_code=200, content={"code": 500, "msg": f"数据库操作失败:{exc}", "data": None})
@app.exception_handler(Exception)
async def unhandled_error_handler(request: Request, exc: Exception):
logger.exception("未处理异常:%s %s", request.method, request.url.path)
return JSONResponse(status_code=200, content={"code": 500, "msg": f"服务器内部错误:{exc}", "data": None})
# ==================================================================== 中间件
@app.middleware("http")
async def add_process_time(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
response.headers["X-Process-Time-ms"] = f"{(time.perf_counter() - start) * 1000:.1f}"
return response
# ==================================================================== 路由
app.include_router(api_router)
@app.get("/api/health", tags=["系统"], summary="健康检查")
def health():
return {
"code": 0,
"msg": "ok",
"data": {
"app": settings.APP_NAME,
"version": settings.VERSION,
"database": settings.DB_NAME,
"dialect": engine.dialect.name,
"auth_enabled": settings.AUTH_ENABLED,
},
}
# ==================================================================== 前端
if STATIC_DIR.exists():
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
@app.get("/", include_in_schema=False)
def index():
return FileResponse(str(STATIC_DIR / "index.html"))
if __name__ == "__main__":
import uvicorn
uvicorn.run("app.main:app", host="127.0.0.1", port=8000, reload=False)