一、概述
FastapiAdmin采用微内核 + 动态插件的架构:认证、权限、日志、基础设施这些核心能力作为底座,量化交易、任务调度、代码生成等业务模块以插件形式接入,可以热插拔,也可以按目录解耦。
几个设计要点
- 插件目录只要符合约定,启动时自动扫描挂载,不用在全局路由表手动
include_router。 - ORM 模型不用手动 import,Alembic 迁移时自动遍历插件里的 model 并纳入元数据。
- 插件内部统一按 Model → Schema → CRUD → Service → Controller 五层拆分。
- 响应封装、操作日志、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 迁移或自动建模时:
ImportUtil.find_models(MappedBase)递归遍历工程里的model.py/models.py。- 类继承自
MappedBase且有有效__tablename__的,自动注入MappedBase.metadata。 - 插件里不用在主入口手动
import DemoModel。
五、总结
插件机制本身不复杂:目录约定 + 五层结构 + 自动发现。写新模块时照着 module_example 抄一遍就能跑通。