Skip to content

030. 通用 CRUD Router 与文件上传

学习目标

这一节把后端重复的 CRUD 代码抽成通用封装,并实现文件上传。

学完后,你应该能理解:

  • 为什么要抽 BasicModel
  • 通用 CRUD Router 要接收哪些参数;
  • 为什么通用接口要有统一前缀;
  • ORM 复杂查询为什么写起来不轻;
  • 文件上传有耦合和分离两种方式;
  • 上传文件为什么要保留原文件名;
  • 后端写文件为什么要使用异步文件 API;
  • 本地上传和服务器上传为什么访问结果不一样。

为什么要封装基类

前面已经写过一个 SQLModel 类。

当第二张表、第三张表出现时,会发现很多代码重复:

  • id
  • 创建时间;
  • 更新时间;
  • Pydantic 配置;
  • 字段命名转换;
  • 序列化工具;
  • 基础 CRUD 写法。

这些内容每个 model 都写一遍,会非常冗余。

所以要抽一个基础类,例如 BasicModel

正常情况下可以叫 BaseModel,但 Pydantic 已经有同名类,为了避免冲突,这里改成 BasicModel

BasicModel 放什么

BasicModel 适合放每张表都需要的内容。

例如:

  • 主键 ID;
  • 通用时间字段;
  • Pydantic model 配置;
  • 下划线和驼峰命名转换;
  • 通用序列化方法。

具体表自己的业务字段,仍然放在具体 model 里。

这样新增一张表时,只需要关心它独有的字段。

清理未使用 import

抽基类时,文件里会出现一些不用的 import。

PyCharm 可以通过快捷键自动优化 import。

Windows 下常见是 Ctrl + Alt + O,macOS 下通常有对应快捷键。

这类清理不是形式主义。import 太乱会影响可读性,也可能隐藏真实依赖。

通用 CRUD Router

如果每张表都写一套 list、insert、update、delete 接口,代码会大量重复。

可以封装一个函数,根据 model 类创建一组通用接口。

它通常需要:

  • model 类;
  • 路由前缀;
  • 查询参数类型;
  • 新建参数类型;
  • 更新参数类型;
  • 删除逻辑;
  • 分页逻辑。

这样后续新增表时,可能只需要注册一行配置,就能得到对应 CRUD 接口。

为什么加 general 前缀

通用生成的接口可以加统一前缀,例如 general

这样看到接口路径时,就能知道它属于通用 CRUD。

这是一种约定。

例如只要看到 general 开头,就默认它具备:

  • 分页查询;
  • 新建;
  • 更新;
  • 删除。

命名约定能降低团队沟通成本。

查询参数也要复用

分页查询参数也会重复。

例如:

  • page
  • pageSize
  • 查询条件;
  • 排序信息。

这些参数可以定义成 Pydantic model,并继承通用基类,让它也支持命名转换。

这样前端可以继续传驼峰字段,后端内部仍然保持 Python 和数据库习惯。

Decimal 返回给前端时要注意

后端里用 Decimal 可以避免精度问题。

但返回给前端时,要考虑 JS 数字精度限制。

如果数字特别长,前端用 number 接收可能会丢精度。

常见处理方式是把大数字或 Decimal 序列化成字符串。

这对长 ID、金额、高精度数值都很重要。

复杂查询

ORM 可以用面向对象的方式表达查询条件。

例如:

  • 模糊查询;
  • 日期小于某个值;
  • 多条件组合;
  • or 查询;
  • 多值查询;
  • 包含查询。

普通接口常见做法是把筛选字段平铺在请求体里。

例如:

json
{
  "name": "张",
  "dateStartMax": "2026-01-11"
}

这种写法能满足很多初中级后台需求,但复杂度上来后会变得笨重。

后续通用表格会需要更强的查询表达式,把各种筛选条件统一封装起来。

文件上传的两种方式

后端文件上传大致有两类设计。

方式特点
耦合式文件和表单保存放在同一个业务接口里
分离式文件先上传到静态资源服务,再把文件地址保存到业务表

当前更偏分离式。

先把文件上传到指定目录,返回文件访问路径;业务表单再保存这个路径。

这种方式更灵活,也更适合前端组件化。

文件保存路径和访问路径

文件上传需要两个配置。

配置作用
文件保存路径文件实际存到服务器磁盘哪里
文件访问路径浏览器通过什么 URL 访问这个文件

保存路径是服务器内部路径。

访问路径是对外 URL。

不要把这两个概念混在一起。

为什么每个文件放进独立目录

上传文件时,项目会生成一个唯一 ID,把文件放到这个 ID 对应的目录里。

这样可以保留原文件名。

如果所有文件平铺在同一个目录下,很容易重名。

例如两个用户都上传了 resume.png,后上传的可能覆盖先上传的。

使用独立目录后,路径可以变成:

txt
文件ID/原始文件名

用户下载时仍然能看到原始文件名。

是否要防重复上传

有些系统会通过文件 hash 判断是否重复上传。

但这不是所有项目都必须做。

是否要做取决于业务:

  • 是否担心用户重复上传;
  • 是否要节省存储;
  • 是否要识别相同文件;
  • 是否允许同名不同内容文件。

当前阶段先实现基础上传流程。

水印和缩略图

有些企业要求图片上传后加水印。

水印可能包含:

  • 用户名;
  • 职位;
  • 拍照时间;
  • 定位信息。

这类能力通常更适合交给 OSS / COS 这类对象存储服务。

前端用 canvas 做水印会有兼容性问题;后端自己处理图片会消耗大量内存和 CPU。

对象存储还可以更方便地生成大图、中图、小图。

学习项目里为了节省资源,先使用 Nginx 直接托管静态文件。

生成唯一 ID

上传文件时需要唯一 ID。

项目里会封装 nextId 之类的函数,通过数据库生成唯一 ID。

这样能保证 ID 在数据库层面不重复。

一次也可以生成多个 ID,用来支持批量场景。

FastAPI 接收上传文件

FastAPI 提供 UploadFile 类型。

接口参数使用 UploadFile 后,FastAPI 会把上传的文件注入到参数里。

你可以从这个对象里读取:

  • 文件名;
  • 文件内容;
  • 文件类型;
  • 文件流。

这比自己解析 multipart 请求简单很多。

异步写文件

写入上传文件时,不建议使用同步 open 直接写。

同步文件写入会阻塞后端应用。

可以使用异步文件库,例如 aiofiles,用异步方式把文件写到磁盘。

这一点和前面讲同步阻塞是一条线:后端接口里尽量避免长时间阻塞事件循环。

本地上传和服务器访问

如果后端服务跑在本地,上传文件会存到本地电脑。

但前端线上页面访问的是服务器上的资源路径。

所以线上调试时,如果前端要访问上传后的图片,后端也应该部署到服务器上。

否则会出现:

  • 上传接口返回了路径;
  • 但路径对应的是本地文件;
  • 浏览器访问服务器地址时找不到这个文件。

这不是上传代码坏了,而是运行环境不一致。

部署上传服务

部署后端上传服务时,要同步处理:

  • 后端代码发布;
  • Conda 环境;
  • 依赖安装;
  • .env 配置;
  • 文件保存目录;
  • Nginx 静态资源访问;
  • 云服务器端口;
  • 前端请求地址。

如果还有另一个 AI 简历预览项目,需要避免端口冲突。

可以给不同后端服务分配不同端口,并在前端环境变量里分别配置。

这一节的重点

这一节完成了两个关键抽象。

第一是通用 CRUD:

  • BasicModel
  • 抽通用 Router;
  • 减少重复接口;
  • 为后续配置化表格做准备。

第二是文件上传:

  • 区分保存路径和访问路径;
  • 保留原文件名;
  • 使用唯一 ID;
  • 用 FastAPI 接收文件;
  • 用异步方式写入;
  • 部署后在服务器环境验证。

后面 AI 简历会用到文件上传能力,通用业务表格也会继续沿用这些后端基础。

AI Agent 课程学习文档。