




















上次我们终于把 FastapiAdmin 跑起来了,界面真不错,RBAC、菜单管理、日志监控一应俱全。心想这得省多少事啊。
但接下来需求来了:“加个会议纪要模块呗,能增删改查就行。”
你盯着文件夹看了半小时,根本不知道第一行代码该写在哪。
最后我也是硬着头皮翻了N个源码文件,才摸清它的“脾气”。
✅ 看懂 FastapiAdmin 后端的真实目录结构(和你想的不一样)
✅ 手把手新增一个完整的业务模块(model → schema → crud → service → controller)
✅ 避开路由注册、权限集成和前端联调的深坑
真实项目结构一览
➡️ 二次开发标准流程
➡️ 实战:增加“会议纪要”模块
➡️ 常见翻车现场与避坑指南
我当初 git clone 下来,看到的是这样的:
FastapiAdmin/
├── backend/ # 后端工程,我们的主战场
│ └── app/
│ ├── core/ # 核心工具库
│ ├── config/ # Settings
│ ├── utils/ # 通用工具类
│ ├── scripts/# 启动脚本
│ ├── plugin/ # 动态路由
│ └── api/ # 静态路由
│ └── v1/
│ ├── module_system/
│ ├── module_monitor/
│ ├── module_common/
│ ├── module_application/
│ └── portal/ # 一个完整的模块示例
│ ├── controller.py # 路由与请求处理
│ ├── crud.py # 数据库增删改查
│ ├── model.py # SQLAlchemy 模型
│ ├── schema.py # Pydantic 校验
│ └── service.py # 业务逻辑
├── frontend/ # Vue3 前端工程
└── docker/ # Docker部署相关
看到没,它是把一个业务模块的所有东西打成一个小包,放在一个文件夹里,跟常见的那种 models/ apis/ services/ 分开平铺的结构完全不同。
你可能会问:“那我要新增一个模块怎么办?”照着 portal 复制一份,改吧改吧就行了,后面我一步步说。
捋一下每个文件的职责,心里先有个谱:
🔹 model.py — 定义数据库表结构,就是 SQLAlchemy 的模型类。
🔹 schema.py — 接口的请求/响应数据结构,用 Pydantic 定义。
🔹 crud.py — 只管和数据库打交道,增删改查全都放这里。
🔹 service.py — 业务逻辑层,比如创建纪要前要校验会议时间是否冲突。
🔹 controller.py — API 路由,接收请求、调 service、返回响应。
这个分法很干净,维护起来特别舒服。我一开始还想把逻辑全部塞到 controller 里,后来改需求改到崩溃,千万别学我当初偷懒。
当然,说是一个都不能少,如果你只是个简单的接口响应返回,只有一个 controller 也是Ok的!
增删改查会议纪要,字段:标题、参会人员、纪要内容、会议日期。
在 module_application 下复制 portal 文件夹,重命名为 meeting,里面原有文件清空,咱们从头写。
from sqlalchemy import Column, Integer, String, Date, Text
from app.core.base_model import ModelMixin, UserMixin # 注意这个导入路径,根据实际情况调整
class MeetingMinutes(ModelMixin, UserMixin):
__tablename__ = "meeting_minutes"
title = Column(String(200), nullable=False, comment="会议标题")
attendees = Column(String(500), nullable=False, comment="参会人员")
content = Column(Text, nullable=True, comment="纪要内容")
meeting_date = Column(Date, nullable=False, comment="会议日期")
这里有个坑:一定要继承项目自己的 base_model,它把 id、create_time 这些通用字段全封装好了,别自己再定义一遍,不然字段冲突搞得你怀疑人生。
from app.core.base_schema import BaseSchema
from datetime import date
class MeetingCreate(BaseSchema):
title: str
attendees: str
content: str | None = None
meeting_date: date
class MeetingUpdate(MeetingCreate):
pass
class MeetingOut(MeetingCreate):
id: int
create_time: str
class Config:
from_attributes = True
from app.core.base_crud import CRUDBase
from .model import MeetingMinutes
from .schema import MeetingCreate, MeetingUpdate, MeetingOut
class MeetingCRUD(CRUDBase[MeetingMinutes, MeetingCreate, MeetingUpdate]):
def __init__(self, auth: AuthSchema) -> None:
"""
初始化CRUD数据层,在CRUDBase中已封装了数据库的常用操作
"""
super().__init__(model=MeetingMinutes)
async def get_list(
self,
search: dict | None = None,
order_by: list[dict] | None = None,
preload: list[str] | None = None,
) -> Sequence[MeetingMinutes]:
"""
列表查询
参数:
- search (dict | None): 查询参数
- order_by (list[dict] | None): 排序参数
- preload (list[str] | None): 预加载关系,未提供时使用模型默认项
返回:
- Sequence[MeetingMinutes]: 模型实例序列
"""
return await self.list(search=search, order_by=order_by, preload=preload)
async def create(self, data: MeetingCreate) -> MeetingMinutes | None:
return await self.create(data=data)
async def update(self, id: int, data: MeetingUpdate) -> MeetingMinutes | None:
return await self.update(id=id, data=data)
这要要注意:如果遇到要操作数据库,先去 CRUDBase 里面看看有没有已经封装好的方法,如果有,就不要再造轮子了,直接传参调用即可!
from .crud import MeetingCRUD
from .schema import MeetingCreate, MeetingUpdate, MeetingOut
class MeetingService:
@classmethod
async def create_meeting(cls, data: MeetingCreate):
# 这里可以加业务校验,比如会议时间不能早于今天
return await MeetingCRUD.create(data=data)
@classmethod
async def update_meeting(cls, meeting_id: int, data: MeetingUpdate):
return await MeetingCRUD.update(id=meeting_id, data=data)
from fastapi import APIRouter
from .service import MeetingService
from .schema import MeetingCreate, MeetingUpdate, MeetingOut
from app.common.response import ResponseSchema, SuccessResponse
MeetingRouter = APIRouter(route_class=OperationLogRoute, prefix="/meeting", tags=["会议纪要"])
@MeetingRouter.post("/", response_model=ResponseSchema[MeetingOut])
async def create_meeting(data: MeetingCreate):
result_dict = await MeetingService.create_metting(data=data)
log.info(f"创建成功: {result_dict.get('title')}")
return SuccessResponse(data=result_dict, msg="创建成功")
去 module_application 下的初始化包文件 __init_.py 里,加上:
from .metting.controller import MettingRouter
application_router.include_router(MettingRouter)
我当初写好 controller 启动服务,结果 404,查了半天才发现路由压根没注册。
但不知道你有没有注意到项目目录结构里有个 plugin 目录,我在 scripts/init_app.py 里的 register_routers() 方法里看到了这句代码:
# 先将动态路由注册到应用,使用速率限制器
from app.core.discover import get_dynamic_router
# 获取动态路由实例
app.include_router(
router=get_dynamic_router(),
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
)
进入方法里面看细节,发现如果把整个自定义应用包放到 plugin 目录里,在初始化应用时,会自动查找包里的 controller 里的 Router 定义并自动载入到应用中,这妥妥的插件化开发呀!
🔴 数据库迁移别手动改表:FastapiAdmin 用了 Alembic,写完 model 记得跑 uv run main.py revision --env=dev,不然上线后表结构对不上,哭都来不及。
🔴 权限校验别忘加:新模块接口默认不挂权限,得去 RBAC 菜单管理里配上,否则用户连 403 都报不出来,直接 404 让你找半天。
🔴 前端菜单要手动配:后端只管接口,左侧菜单栏的入口得去前端菜单管理页面手动添加,不然数据能查但用户找不到入口,还以为你没做。
FastapiAdmin 这种全栈脚手架,最大的价值不是代码多厉害,而是它逼着你按一套清晰的套路出牌。model → schema → crud → service → controller 这条链走顺了,后面加再多的模块都不怕。
但最大的坑也恰恰是——你千万别想跳过任何一个文件,少写一个 crud,把逻辑全扔 controller 里,后期维护起来那叫一个酸爽。
建议动手前,把 portal 模块里的五个文件从头到尾读一遍,心里有数了再开工。这里就别学我第一次上来就复制粘贴改改改,结果路径全错,debug 的时间比重新写都长😅。
💬 如果你也正在用 FastapiAdmin 搞二次开发,或者卡在某一步进行不下去,留言说说你的场景。点赞收藏加关注,我们后续接着聊聊其他FastAPI开发中的那些经验技巧与避坑指南~
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。