- Permission 模型新增 sort 字段
- 后端 API 按 sort 升序排列树形菜单
- 前端菜单管理页面增加排序列
- 初始数据添加排序号
- 修复 GET /permissions/{id} 详情接口 bug
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
183 lines
6.2 KiB
Python
183 lines
6.2 KiB
Python
from fastapi import FastAPI, HTTPException
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.staticfiles import StaticFiles
|
||
from fastapi.responses import HTMLResponse, FileResponse
|
||
from fastapi.exceptions import RequestValidationError
|
||
from contextlib import asynccontextmanager
|
||
from apps.urls import api_router
|
||
from apps.settings.urls import UPLOAD_DIR
|
||
from database import engine, get_db
|
||
from models import Base
|
||
from init_data import init_all_data
|
||
from pathlib import Path
|
||
from starlette.requests import Request
|
||
from starlette.responses import JSONResponse
|
||
from sqlalchemy import text
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
# 启动时初始化数据库
|
||
async with engine.begin() as conn:
|
||
# 创建所有表
|
||
await conn.run_sync(Base.metadata.create_all)
|
||
# 为已有表添加 sort 列(兼容 PostgreSQL 和 SQLite)
|
||
try:
|
||
await conn.execute(text("ALTER TABLE permission ADD COLUMN IF NOT EXISTS sort INTEGER DEFAULT 0"))
|
||
except Exception:
|
||
# SQLite 不支持 IF NOT EXISTS
|
||
try:
|
||
await conn.execute(text("ALTER TABLE permission ADD COLUMN sort INTEGER DEFAULT 0"))
|
||
except Exception:
|
||
pass # 列已存在则忽略
|
||
|
||
# 初始化数据
|
||
async for db in get_db():
|
||
await init_all_data(db)
|
||
break
|
||
|
||
yield
|
||
# 关闭时的清理工作
|
||
await engine.dispose()
|
||
|
||
|
||
# 实例化FastAPI
|
||
app = FastAPI(
|
||
title="图书系统授权服务API",
|
||
description="""图书系统授权服务API是一套用于用户认证和授权的服务,提供用户注册、登录、权限校验等功能。
|
||
同时实现了基于角色的访问控制(RBAC),支持自定义角色和权限。还增加了日志记录功能,方便监控和调试。""",
|
||
version="1.0.0",
|
||
lifespan=lifespan,
|
||
docs_url=None,
|
||
redoc_url="/redoc",
|
||
)
|
||
|
||
# 开发环境前后端分离时的跨域(生产环境请收窄 allow_origins)
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=[
|
||
"http://localhost:5173",
|
||
"http://127.0.0.1:5173",
|
||
"http://localhost:3000",
|
||
"http://127.0.0.1:3000",
|
||
],
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
expose_headers=["X-Captcha-ID"],
|
||
)
|
||
|
||
# 挂载swagger-ui静态文件目录
|
||
swagger_ui_path = Path(__file__).parent.parent / "swagger-ui"
|
||
app.mount("/static", StaticFiles(directory=str(swagger_ui_path)), name="static")
|
||
|
||
# 上传图片通过自定义端点提供(和 settings/urls.py 中的 UPLOAD_DIR 保持一致)
|
||
@app.get("/uploads/{filename}")
|
||
async def serve_upload(filename: str):
|
||
"""提供上传的图片文件"""
|
||
file_path = UPLOAD_DIR / filename
|
||
if not file_path.exists():
|
||
raise HTTPException(status_code=404, detail="文件不存在")
|
||
return FileResponse(str(file_path))
|
||
|
||
|
||
# 自定义Swagger UI页面
|
||
@app.get("/docs", include_in_schema=False)
|
||
async def custom_swagger_ui_html():
|
||
html_content = """
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Book System API - Swagger UI</title>
|
||
<link rel="stylesheet" type="text/css" href="/static/swagger-ui.css">
|
||
<link rel="icon" type="image/png" href="/static/favicon-32x32.png" sizes="32x32"/>
|
||
<style>
|
||
html {
|
||
box-sizing: border-box;
|
||
overflow: -moz-scrollbars-vertical;
|
||
overflow-y: scroll;
|
||
}
|
||
*, *:before, *:after {
|
||
box-sizing: inherit;
|
||
}
|
||
body {
|
||
margin: 0;
|
||
background: #fafafa;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="swagger-ui"></div>
|
||
<script src="/static/swagger-ui-bundle.js"></script>
|
||
<script>
|
||
window.onload = function() {
|
||
const ui = SwaggerUIBundle({
|
||
url: "/openapi.json",
|
||
dom_id: '#swagger-ui',
|
||
deepLinking: true,
|
||
presets: [
|
||
SwaggerUIBundle.presets.apis,
|
||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||
],
|
||
layout: "BaseLayout",
|
||
persistAuthorization: true,
|
||
docExpansion: "list"
|
||
});
|
||
window.ui = ui;
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|
||
"""
|
||
return HTMLResponse(content=html_content)
|
||
|
||
|
||
# 包含路由
|
||
app.include_router(api_router)
|
||
|
||
|
||
# 声明装饰器方法和路径
|
||
@app.get("/")
|
||
# 声明装饰器函数
|
||
async def home():
|
||
return {"message": "Hello World!!!"}
|
||
|
||
|
||
# 自定义验证错误处理器
|
||
@app.exception_handler(RequestValidationError)
|
||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||
errors = []
|
||
for err in exc.errors():
|
||
field = err.get("loc", ["unknown"])[-1]
|
||
msg = err.get("msg", "验证失败")
|
||
|
||
if "min_length" in msg.lower():
|
||
if field == "username":
|
||
msg = "用户名长度至少为3个字符"
|
||
elif field == "password":
|
||
msg = "密码长度至少为6个字符"
|
||
elif "max_length" in msg.lower():
|
||
if field == "username":
|
||
msg = "用户名长度不能超过50个字符"
|
||
elif field == "password":
|
||
msg = "密码长度不能超过50个字符"
|
||
|
||
errors.append({"field": field, "message": msg})
|
||
|
||
return JSONResponse(
|
||
status_code=422,
|
||
content={"code": 422, "message": "验证失败", "errors": errors}
|
||
)
|
||
|
||
|
||
# 如果使用命令行启动,使用uvicorn 文件名:app --reload启动即可,下面命令就不用写
|
||
# 如果写下面的命令,就不需要命令行启动了,直接用IDE运行即可
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
import os
|
||
|
||
name = f"{os.path.splitext(os.path.basename(os.path.abspath(__file__)))[0]}:app"
|
||
uvicorn.run(name, host="0.0.0.0", port=8000, reload=True, reload_dirs=["_"])
|