FastapiAdmin插件介绍

0 阅读6分钟

一、概述

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 目录结构

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.pydemo/controller.py扫描目标
APIRouter 实例在 controller.py 顶层定义并赋值给变量DemoRouter = APIRouter(prefix="/demo", ...)最终请求路径:/example/demo/...

三、插件各层组件详解

3.1 插件元数据(plugin.toml)

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

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:

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 定义请求体、响应体和查询参数:

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,常规增删改查不用自己写:

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)

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

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 契约,挂依赖注入(鉴权、数据库会话)和操作日志切面:

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)时自动装配路由:

image.png

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

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

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

五、总结

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