切换日光/暗黑模式
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 简历会用到文件上传能力,通用业务表格也会继续沿用这些后端基础。