FastapiAdmin插件介绍

一、概述

FastapiAdmin采用微内核 + 动态插件的架构:认证、权限、日志、基础设施这些核心能力作为底座,量化交易、任务调度、代码生成等业务模块以插件形式接入,可以热插拔,也可以按目录解耦。

几个设计要点

  1. 插件目录只要符合约定,启动时自动扫描挂载,不用在全局路由表手动 include_router。
  2. ORM 模型不用手动 import,Alembic 迁移时自动遍历插件里的 model 并纳入元数据。
  3. 插件内部统一按 Model → Schema → CRUD → Service → Controller 五层拆分。
  4. 响应封装、操作日志、RBAC 权限校验、Excel 导入导出这些底座能力直接用。

二、插件结构

以 backend/app/plugin/module_example 为例,目录结构如下:

2.1 目录结构

bash 复制代码
backend/app/plugin/module_example/
├── __init__.py                # Python 包声明(必须)
├── plugin.toml                # 插件元信息描述文件
└── demo/                      # 具体业务子模块(可存在多个)
    ├── __init__.py            # 子包声明
    ├── model.py               # ORM 数据模型层 (SQLAlchemy)
    ├── schema.py              # 数据校验与传输模型层 (Pydantic V2)
    ├── crud.py                # 基础数据库操作层 (CRUDBase)
    ├── service.py             # 业务逻辑与编排层
    └── controller.py          # HTTP 控制器与路由定义 (FastAPI)

2.2 命名与路由映射

路由发现器 DynamicRouterRegistry(app.core.discover)的命名规则:

命名位置 规范要求 示例 映射结果
插件根目录 必须位于 app/plugin/ 下,且以 module_ 开头 module_example 自动剥离前缀,映射为根路径 /example
目录名称 从 module_xxx 到 controller.py 每一级必须是合法 Python 标识符 module_example/demo 合法包路径 app.plugin.module_example.demo
控制器文件 必须命名为 controller.py demo/controller.py 扫描目标
APIRouter 实例 在 controller.py 顶层定义并赋值给变量 DemoRouter = APIRouter(prefix="/demo", ...) 最终请求路径:/example/demo/...

三、插件各层组件详解

3.1 插件元数据(plugin.toml)

plugin.toml 存放插件的描述性元信息,控制台、文档中心、运维门户都会读取:

ini 复制代码
name = "example"
title = "示例插件"
version = "1.0.0"
description = "演示 module_* 目录约定与动态路由注册(demo)。"
optional = true
tags = ["demo", "sample"]
  • name:插件唯一标识。
  • optional:是否可选插件(按需加载或裁剪部署用)。
  • tags:分类标签。

3.2 模型层(model.py)

用 SQLAlchemy 2.0+ 的 Mapped / mapped_column 写法,继承核心 Mixin:

ini 复制代码
from datetime import date, datetime, time
from sqlalchemy import BIGINT, JSON, Boolean, Date, DateTime, Float, Integer, String, Text, Time
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin

class DemoModel(ModelMixin, UserMixin):
    """示例表 - 涵盖常用字段类型与审计字段"""

    __tablename__: str = "example_demo"
    __table_args__: dict[str, str] = {"comment": "示例表"}

    # 业务字段
    name: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="名称")
    status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启用 1:停用)", index=True)
    description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
    int_val: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="整数")
    bigint_val: Mapped[int | None] = mapped_column(BIGINT, nullable=True, comment="大整数")
    float_val: Mapped[float | None] = mapped_column(Float, nullable=True, comment="浮点数")
    bool_val: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment="布尔型")
    date_val: Mapped[date | None] = mapped_column(Date, nullable=True, comment="日期")
    time_val: Mapped[time | None] = mapped_column(Time, nullable=True, comment="时间")
    datetime_val: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, comment="日期时间")
    text_val: Mapped[str | None] = mapped_column(Text, nullable=True, comment="长文本")
    json_val: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="元数据(JSON格式)")

Mixin 自带字段:

  • ModelMixin:主键 id、逻辑删除 del_flag、created_time、updated_time 等基准列。
  • UserMixin:创建人 created_id、更新人 updated_id 等审计字段。

3.3 Schema 层(schema.py)

基于 Pydantic V2 定义请求体、响应体和查询参数:

python 复制代码
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
from app.core.validator import DateStr, DateTimeStr, TimeStr

class DemoCreateSchema(BaseModel):
    """创建请求模型"""
    name: str = Field(..., description="名称")
    status: int = Field(default=0, ge=0, le=1, description="是否启用(0:启用 1:禁用)")
    description: str | None = Field(default=None, description="描述")
    # ... 其他字段

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        v = v.strip()
        if not v:
            raise ValueError("名称不能为空")
        return v

    @model_validator(mode="after")
    def _after_validation(self):
        if len(self.name) < 2 or len(self.name) > 50:
            raise ValueError("名称长度必须在2-50个字符之间")
        return self

class DemoUpdateSchema(BaseModel):
    """更新请求模型(所有字段均设为可选)"""
    name: str | None = Field(default=None, description="名称")
    status: int | None = Field(default=None, ge=0, le=1, description="是否启用(0:启用 1:禁用)")
    description: str | None = Field(default=None, description="描述")

class DemoOutSchema(DemoCreateSchema, BaseSchema, UserBySchema):
    """响应模型:支持 ORM 模式自动映射"""
    model_config = ConfigDict(from_attributes=True)

class DemoQueryParam(BaseQueryParam, UserByQueryParam):
    """列表查询参数:支持字段级的条件匹配规则(如 like, eq, in 等)"""
    name: str | None = Field(None, description="名称", json_schema_extra={"q": "like"})
    description: str | None = Field(None, description="描述", json_schema_extra={"q": "like"})
    status: int | None = Field(None, description="是否启用", json_schema_extra={"q": "eq"})

3.4 CRUD 层(crud.py)

继承 CRUDBase,常规增删改查不用自己写:

python 复制代码
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema
from .model import DemoModel
from .schema import DemoCreateSchema, DemoUpdateSchema

class DemoCRUD(CRUDBase[DemoModel, DemoCreateSchema, DemoUpdateSchema]):
    """示例数据访问层"""

    def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
        super().__init__(model=DemoModel, auth=auth, db=db)

CRUDBase 提供:

get(), get_list(), page(), create(), update(), delete(), set() 等,并自动挂接当前用户认证信息与逻辑删除过滤。

3.5 Service 层(service.py)

业务校验、异常抛出、第三方工具调用、流程编排都放这一层:

python 复制代码
from typing import Any
from fastapi import UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.exceptions import CustomException
from app.utils.common_util import search_to_dict
from app.utils.excel_util import ExcelUtil
from .crud import DemoCRUD
from .schema import DemoCreateSchema, DemoOutSchema, DemoQueryParam, DemoUpdateSchema

class DemoService:
    def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
        self.auth = auth
        self.db = db

    async def detail(self, id: int) -> DemoOutSchema:
        obj = await DemoCRUD(self.auth, self.db).get(id=id)
        if not obj:
            raise CustomException(msg="该数据不存在")
        return DemoOutSchema.model_validate(obj)

    async def create(self, data: DemoCreateSchema) -> DemoOutSchema:
        # 重复性检查
        obj = await DemoCRUD(self.auth, self.db).get(name=data.name)
        if obj:
            raise CustomException(msg="创建失败,名称已存在")
        obj = await DemoCRUD(self.auth, self.db).create(data=data)
        return DemoOutSchema.model_validate(obj)

    # 更多业务方法:update, delete, page, batch_export, batch_import 等...

3.6 Controller 层(controller.py)

定义 API 契约,挂依赖注入(鉴权、数据库会话)和操作日志切面:

python 复制代码
import urllib.parse
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path, Query, status
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession

from app.common.response import ResponseSchema, SuccessResponse
from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam
from app.core.dependencies import AuthPermission, db_getter
from app.core.router_class import OperationLogRoute

from .schema import DemoCreateSchema, DemoOutSchema, DemoQueryParam, DemoUpdateSchema
from .service import DemoService

# 声明 APIRouter 并配置 OperationLogRoute 审计日志切面
DemoRouter = APIRouter(route_class=OperationLogRoute, prefix="/demo", tags=["示例管理"])

@DemoRouter.get("/list", summary="分页查询示例", response_model=ResponseSchema[PageResultSchema[DemoOutSchema]])
async def get_obj_list_controller(
    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:query"]))],
    page: Annotated[PaginationQueryParam, Depends()],
    search: Annotated[DemoQueryParam, Query()],
    db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
    service = DemoService(auth, db)
    result_dict = await service.page(
        page_no=page.page_no,
        page_size=page.page_size,
        search=search,
        order_by=page.order_by,
    )
    return SuccessResponse(data=result_dict, msg="查询示例列表成功")

@DemoRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建示例", response_model=ResponseSchema[DemoOutSchema])
async def create_obj_controller(
    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:create"]))],
    data: Annotated[DemoCreateSchema, Body(description="创建参数")],
    db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
    service = DemoService(auth, db)
    result_dict = await service.create(data=data)
    return SuccessResponse(data=result_dict, msg="创建示例成功")

四、加载机制

4.1 动态路由自动发现(DynamicRouterRegistry)

应用初始化(init_app)时自动装配路由:

4.2 数据库模型自发现机制(ImportUtil)

执行 Alembic 迁移或自动建模时:

  1. ImportUtil.find_models(MappedBase) 递归遍历工程里的 model.py / models.py。
  2. 类继承自 MappedBase 且有有效 __tablename__ 的,自动注入 MappedBase.metadata。
  3. 插件里不用在主入口手动 import DemoModel。

五、总结

插件机制本身不复杂:目录约定 + 五层结构 + 自动发现。写新模块时照着 module_example 抄一遍就能跑通。

相关推荐
行百里er1 小时前
Redis 核心数据结构(二)——List 与消息队列
redis·后端
知守观1 小时前
AI 代码审查实战:2022年Java老项目挑出20个坑,老炮只认15个
后端
用户051610461671 小时前
1688 / 京东 / 淘宝 item_get 返回字段逐个拆:三个平台的真实报文差在哪
后端
行百里er1 小时前
Redis 核心数据结构(三)——Hash,把一堆字段塞进一个 Key
redis·后端
isfox1 小时前
MapReduce 数据压缩:用 CPU 换 IO,这笔账怎么算?
后端
白雾茫茫丶1 小时前
VibeCoding 一套 Admin 系统,五种技术栈实现
前端·ai编程·vibecoding
Yeyu1 小时前
不靠 libaums自己如何在 Android 上用 USB 协议把 U 盘读出来
前端
能掘金的小能手1 小时前
「数据库连不上」——一次误判,和它暴露出的排查盲区
后端
夏天要喝冰可乐1 小时前
Trae 每天自动签到:Serverless 定时任务完整复盘
前端·python