• 给 FastApiAdmin 加个“会议纪要”模块,我把后端二次开发的坑踩了个遍


    😫 先吐个槽:项目跑起来了,但要加个功能模块,从哪下手?

    上次我们终于把 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.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 这条链走顺了,后面加再多的模块都不怕。

    但最大的坑也恰恰是——你千万别想跳过任何一个文件,少写一个 crud,把逻辑全扔 controller 里,后期维护起来那叫一个酸爽。

    建议动手前,把 portal 模块里的五个文件从头到尾读一遍,心里有数了再开工。这里就别学我第一次上来就复制粘贴改改改,结果路径全错,debug 的时间比重新写都长😅。


    💬 如果你也正在用 FastapiAdmin 搞二次开发,或者卡在某一步进行不下去,留言说说你的场景。点赞收藏加关注,我们后续接着聊聊其他FastAPI开发中的那些经验技巧与避坑指南~

  • 相关阅读:
    py 时候同时打开多个页面,操作多个页面
    分布式ID选型对比(2)
    由浅入深学会白盒测试用例设计
    【图像配准】基于surf算法实现图像配准附Matlab代码
    【长文】带你搞明白Redis
    一文详解Docker数据卷(volume)
    【Python】【机器学习】【机器视觉】PCA和LDA降维学习【附源码】【新加坡南洋理工大学】
    Java开发学习(四十三)----MyBatisPlus查询语句之查询投影
    R语言将多个字符串连接起来:使用paste函数将多个字符串拼接起来、使用paste函数将字符串和数值计算结果拼接起来
    Linux 文件读写
  • 原文地址:https://www.cnblogs.com/ymtianyu/p/20154680