给 FastApiAdmin 加个“会议纪要”模块,我把后端二次开发的坑踩了个遍
本文能帮你解决什么
✅ 看懂 FastapiAdmin 后端的真实目录结构(和你想的不一样)
✅ 手把手新增一个完整的业务模块(model → schema → crud → service → controller)
✅ 避开路由注册、权限集成和前端联调的深坑
🧭 主要内容脉络
真实项目结构一览
➡️ 二次开发标准流程
➡️ 实战:增加“会议纪要”模块
➡️ 常见翻车现场与避坑指南
1. 先搞懂真实的项目结构,不然代码都不知道放哪
我当初 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 复制一份,改吧改吧就行了,后面我一步步说。
2. 二次开发的标准流程:五个文件,一个都不能少
捋一下每个文件的职责,心里先有个谱:
🔹model.py— 定义数据库表结构,就是 SQLAlchemy 的模型类。
🔹schema.py— 接口的请求/响应数据结构,用 Pydantic 定义。
🔹crud.py— 只管和数据库打交道,增删改查全都放这里。
🔹service.py— 业务逻辑层,比如创建纪要前要校验会议时间是否冲突。
🔹controller.py— API 路由,接收请求、调 service、返回响应。
这个分法很干净,维护起来特别舒服。我一开始还想把逻辑全部塞到 controller 里,后来改需求改到崩溃,千万别学我当初偷懒。
当然,说是一个都不能少,如果你只是个简单的接口响应返回,只有一个 controller 也是Ok的!
3. 实战演示:手把手增加“会议纪要”模块
📋 需求:
增删改查会议纪要,字段:标题、参会人员、纪要内容、会议日期。
🔹 第1步:新建模块文件夹
在 module_application 下复制 portal 文件夹,重命名为 meeting,里面原有文件清空,咱们从头写。
🔹 第2步:写 model.py
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 这些通用字段全封装好了,别自己再定义一遍,不然字段冲突搞得你怀疑人生。
🔹 第3步:写 schema.py
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🔹 第4步:写 crud.py
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 里面看看有没有已经封装好的方法,如果有,就不要再造轮子了,直接传参调用即可!
🔹 第5步:写 service.py
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)🔹 第6步:写 controller.py
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="创建成功")🔹 第7步:注册路由(最容易漏!)
去 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 定义并自动载入到应用中,这妥妥的插件化开发呀!
4. 常见翻车现场与避坑指南
🔴数据库迁移别手动改表:FastapiAdmin 用了 Alembic,写完 model 记得跑 uv run main.py revision --env=dev,不然上线后表结构对不上,哭都来不及。
🔴权限校验别忘加:新模块接口默认不挂权限,得去 RBAC 菜单管理里配上,否则用户连 403 都报不出来,直接 404 让你找半天。
🔴前端菜单要手动配:后端只管接口,左侧菜单栏的入口得去前端菜单管理页面手动添加,不然数据能查但用户找不到入口,还以为你没做。
5. 我的血泪总结
FastapiAdmin 这种全栈脚手架,最大的价值不是代码多厉害,而是它逼着你按一套清晰的套路出牌。model → schema → crud → service → controller 这条链走顺了,后面加再多的模块都不怕。