上次我们终于把 FastapiAdmin 跑起来了界面真不错RBAC、菜单管理、日志监控一应俱全。心想这得省多少事啊。但接下来需求来了“加个会议纪要模块呗能增删改查就行。”你盯着文件夹看了半小时根本不知道第一行代码该写在哪。最后我也是硬着头皮翻了N个源码文件才摸清它的“脾气”。 本文能帮你解决什么✅ 看懂 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.pyfrom 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), nullableFalse, comment会议标题) attendees Column(String(500), nullableFalse, comment参会人员) content Column(Text, nullableTrue, comment纪要内容) meeting_date Column(Date, nullableFalse, comment会议日期)这里有个坑一定要继承项目自己的 base_model它把 id、create_time 这些通用字段全封装好了别自己再定义一遍不然字段冲突搞得你怀疑人生。 第3步写 schema.pyfrom 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.pyfrom 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__(modelMeetingMinutes) 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(searchsearch, order_byorder_by, preloadpreload) async def create(self, data: MeetingCreate) - MeetingMinutes | None: return await self.create(datadata) async def update(self, id: int, data: MeetingUpdate) - MeetingMinutes | None: return await self.update(idid, datadata)这要要注意如果遇到要操作数据库先去 CRUDBase 里面看看有没有已经封装好的方法如果有就不要再造轮子了直接传参调用即可 第5步写 service.pyfrom .crud import MeetingCRUD from .schema import MeetingCreate, MeetingUpdate, MeetingOut class MeetingService: classmethod async def create_meeting(cls, data: MeetingCreate): # 这里可以加业务校验比如会议时间不能早于今天 return await MeetingCRUD.create(datadata) classmethod async def update_meeting(cls, meeting_id: int, data: MeetingUpdate): return await MeetingCRUD.update(idmeeting_id, datadata) 第6步写 controller.pyfrom fastapi import APIRouter from .service import MeetingService from .schema import MeetingCreate, MeetingUpdate, MeetingOut from app.common.response import ResponseSchema, SuccessResponse MeetingRouter APIRouter(route_classOperationLogRoute, prefix/meeting, tags[会议纪要]) MeetingRouter.post(/, response_modelResponseSchema[MeetingOut]) async def create_meeting(data: MeetingCreate): result_dict await MeetingService.create_metting(datadata) log.info(f创建成功: {result_dict.get(title)}) return SuccessResponse(dataresult_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( routerget_dynamic_router(), dependencies[Depends(RateLimiter(times5, seconds10))], )进入方法里面看细节发现如果把整个自定义应用包放到 plugin 目录里在初始化应用时会自动查找包里的 controller 里的 Router 定义并自动载入到应用中这妥妥的插件化开发呀4. 常见翻车现场与避坑指南数据库迁移别手动改表FastapiAdmin 用了 Alembic写完 model 记得跑 uv run main.py revision --envdev不然上线后表结构对不上哭都来不及。权限校验别忘加新模块接口默认不挂权限得去 RBAC 菜单管理里配上否则用户连 403 都报不出来直接 404 让你找半天。前端菜单要手动配后端只管接口左侧菜单栏的入口得去前端菜单管理页面手动添加不然数据能查但用户找不到入口还以为你没做。5. 我的血泪总结FastapiAdmin 这种全栈脚手架最大的价值不是代码多厉害而是它逼着你按一