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