Skip to content

2026-04-16 合同断点续传与分片上传面试材料

文档定位

这份材料不是实现方案,也不是开发计划,而是给面试时“讲故事”和“扛追问”用的口述稿。

你可以把它分成 4 层来使用:

  1. 先背 3-5 分钟 STAR 主讲稿
  2. 再理解 技术拆解
  3. 然后练 高频追问
  4. 最后用 技术亮点 做拔高

一句话版本

我在既有的 draft-contractchange 两个合同管理页面里,落地了一套基于 Cloudflare R2 multipart 的大文件分片上传和断点续传方案。前端负责切片、并发上传和恢复;Nitro 只负责上传控制面;Neon Postgres 负责记录上传会话和分片状态;最终把上传结果和业务表单提交解耦,形成完整的 CRUD 闭环。


一、3-5 分钟 STAR 主讲稿

下面这段是你可以直接练口述的版本。

1. Situation

我当时做的是合同管理场景,具体有两个既有业务页面,一个是合同草稿 draft-contract,一个是合同变更 change。这两个页面本来都需要支持附件上传,但如果继续走普通的整文件上传,问题会很明显:文件一大就容易失败,网络一断就得整文件重传,编辑态下旧附件、新附件、删除附件也很难统一处理。

2. Task

所以我的任务不是单纯做一个上传组件,而是把“分片上传 + 断点续传 + 业务 CRUD”整套闭环落到这两个页面里。目标有两个:

  1. 大文件上传要稳定,能暂停、恢复、失败重试
  2. 上传结果要能和合同新增、编辑、删除流程真正打通,而不是只做一个 demo

3. Action

我的核心设计是把系统拆成两层。

第一层是数据面,也就是文件本身怎么传。我没有让大文件经过业务服务端中转,而是让浏览器直接把每个分片上传到 Cloudflare R2。这样服务端不会承受大文件流量压力,上传性能和成本都更合理。

第二层是控制面,也就是谁来管理上传过程。我用 Nitro 来负责创建上传会话、查询已上传分片、给每个分片签发临时上传 URL、最后完成 multipart merge。然后我用 Neon Postgres 记录上传会话表和分片表,这样刷新页面后还能恢复现场,做到断点续传。

前端这边,我把上传能力抽成了一个共享的状态机和上传组件,统一服务 draft-contractchange 两个页面。状态机里会维护文件指纹、会话 ID、分片状态、上传进度、暂停/恢复/失败重试这些状态。

另外一个比较关键的点是,我没有把“上传完成”和“业务提交成功”绑死在一起。上传完成只代表对象已经成功放到 R2,但真正写入正式附件表,是在合同的 create 或 update 成功时才物化。这么做是为了避免出现“文件已经传上去了,但业务主记录还没创建成功”的脏数据问题。

change 页面里,我还处理了编辑态附件差量维护的问题,也就是旧附件有的保留、有的删除,同时又新增了一批新附件。这个场景比单纯上传更贴近真实业务。

4. Result

最后这套方案在两个合同页面里都形成了完整闭环:支持分片上传、断点恢复、附件新增、编辑、删除,以及编辑态附件差量更新。它的价值不只是“上传成功”,而是把上传真正融进了业务流程里,同时架构上也比较清晰,前端、服务端、对象存储、数据库各自职责分明。


二、你先要搞懂的核心概念

这一部分不是给面试官背的,是给你先学会这套技术。

1. 什么叫分片上传

分片上传就是把一个大文件拆成很多小块,每一块单独上传。

比如一个 100MB 文件,如果每片 5MB,那就会拆成 20 片。
前端不会一次性传 100MB,而是传第 1 片、第 2 片、第 3 片……

这样做的好处是:

  • 单片失败时,只需要重传这一片
  • 可以做并发上传,提高速度
  • 可以记录每片是否成功,支持断点续传

2. 什么叫断点续传

断点续传的重点不是“暂停后继续”这四个字,而是:

前端和服务端都知道“这个文件已经传到第几片了”。

比如一个文件共 20 片:

  • 前 12 片已经上传成功
  • 第 13 片失败
  • 页面刷新了

这时候如果系统还能找回这个上传会话,并知道前 12 片已经存在,那么它只需要继续传第 13 到 20 片,这就叫断点续传。

3. 为什么一定要有“上传会话”

因为分片上传不是一次请求,而是一组请求。

所以系统必须有一个“会话”来代表:

  • 这是哪个文件
  • 一共多少片
  • 当前已经完成哪些片
  • 这个文件最终要落到 R2 的哪个对象 key
  • R2 返回的 multipart upload ID 是什么

这个“上传会话”就是整套方案的锚点。

4. 为什么不能只靠前端本地记忆

因为只存在浏览器本地还不够:

  • 用户可能刷新页面
  • 浏览器缓存可能丢失
  • 用户可能换一个 tab
  • 还要和真实的云端上传状态对齐

所以真正可靠的做法是:

  • 前端本地缓存一份映射,方便快速恢复
  • 服务端数据库再保存一份权威状态,负责最终对账

三、整体架构怎么讲

你可以把它讲成一句很清楚的话:

我把这套上传方案拆成了“浏览器直传 R2 的数据面”和“Nitro + Neon 的上传控制面”。

1. 数据面

数据面就是文件字节真正怎么传。

这里我选的是:

  • 浏览器切片
  • 浏览器直接 PUT 到 R2
  • 服务端不搬运大文件

你可以这样解释:

大文件最占资源的是文件流本身,所以我不让业务服务端当中转站,而是让浏览器拿着服务端签发的临时 URL 直接上传到 R2。

2. 控制面

控制面就是上传过程怎么被管理。

这里由 Nitro + Neon 来做:

  • init:创建或恢复上传会话
  • status:查询哪些片已经传完
  • sign-part:给某一片签发临时上传 URL
  • complete:把所有片合并成最终对象
  • abort:取消上传

你可以这样说:

服务端不传大文件,但服务端仍然掌控整个上传流程。这样既减轻了服务端流量压力,又保留了对上传权限和最终完成动作的控制。

3. 业务面

业务面就是合同表单本身。

这里我做了一层解耦:

  • 上传完成,只代表文件对象已经在 R2 里
  • 业务提交成功,才会把这些上传结果物化为正式附件记录

这个点很重要,面试官大概率会认可。


四、完整上传链路怎么讲

这一段你要尽量讲顺。

链路步骤

  1. 用户选择文件
  2. 前端按固定 chunkSize 切片
  3. 前端生成一个 resumeFingerprint
  4. 前端调用 upload/init
  5. 服务端创建或返回已有上传会话
  6. 前端调用 upload/status,拿到已完成分片列表
  7. 前端只上传缺失的分片
  8. 每个分片上传前,先调用 upload/sign-part
  9. 服务端返回这个分片的 presigned URL
  10. 浏览器把这个分片直接 PUT 到 R2
  11. 分片成功后拿到 ETag
  12. 所有分片完成后,前端调用 upload/complete
  13. 服务端通知 R2 执行 multipart complete
  14. 服务端返回最终文件 URL、对象 key、会话 ID
  15. 前端把这些结果放进表单附件数组
  16. 用户最后点击业务提交
  17. 业务 create/update 接口再把这些上传结果物化成正式附件记录

这一段的人话解释

你可以这么总结:

我把上传拆成了两阶段。第一阶段是“文件对象上传完成”,第二阶段是“业务提交成功并关联附件”。这样能避免脏数据,也更符合真实业务流程。


五、表设计为什么要分三层

这一部分特别适合面试官追问。

1. ct_upload_sessions

这张表是上传会话主表。

它记录的是:

  • 这个文件是谁
  • 属于哪个业务类型
  • 文件名、大小、MIME 类型
  • 分片大小、总分片数
  • 对应的 R2 uploadId
  • 当前状态
  • 最终对象 key 和 public URL

你可以这样解释:

这张表负责描述“一次上传任务”的整体状态。

2. ct_upload_session_parts

这张表是分片明细表。

它记录的是:

  • 第几片
  • 这一片的 ETag
  • 这一片多大
  • 什么时候上传成功

你可以这样解释:

断点续传的关键不是知道“文件传了一半”,而是知道“具体哪几片已经成功”。所以我单独拆了分片表。

3. ct_attachments

这张表是最终业务附件表。

它记录的是:

  • 附件属于哪个合同或合同变更
  • 最终对象 URL
  • 对象 key
  • MIME 类型
  • 存储提供方
  • 来源 uploadSessionId

你可以这样解释:

ct_upload_sessions 是上传过程数据,ct_attachments 是业务最终结果数据,这两者不能混在一起。

4. 为什么不能只用一张表

因为一张表会把“过程态”和“结果态”混在一起,问题很多:

  • 文件还没传完,业务表就被污染
  • 很难表示某一片失败
  • 很难表示已上传但尚未绑定业务主记录
  • 编辑态下也很难维护清楚

一句话回答:

我是故意把过程态和结果态拆开的,这样状态表达更完整,也更容易做恢复和回收。


六、前端断点续传状态机怎么讲

面试官如果偏前端,会很喜欢这一段。

我定义的核心状态

  • idle
  • fingerprinting
  • initiating
  • uploading
  • paused
  • resuming
  • completing
  • completed
  • failed
  • aborted

为什么要有状态机

因为上传不是一个普通按钮点击动作,而是一个长流程。

如果没有状态机,就会很混乱:

  • 什么时候能提交
  • 什么时候能暂停
  • 什么时候应该重试
  • 什么时候要阻止关闭弹窗
  • 什么时候附件可以被业务表单消费

你可以这样说:

我把上传过程建模成状态机,本质上是为了把 UI 行为、网络行为和业务提交行为统一起来。

前端还做了什么

  • 用本地缓存保存 resumeFingerprint -> sessionId
  • 页面刷新后根据 sessionId 去服务端查询真实状态
  • 只补传缺失分片
  • 支持暂停、恢复、失败重试
  • 统一抽成共享组件,供两个页面复用

七、为什么 change 页面比 draft-contract 更有技术含量

这个点很适合你在面试里主动拔高。

draft-contract

它相对直接:

  • 新增合同草稿
  • 编辑合同草稿
  • 上传新附件
  • 删除附件

change

它复杂在编辑态差量维护。

因为用户编辑一个合同变更时,附件可能同时存在三类:

  • 旧附件,继续保留
  • 旧附件,这次删除
  • 新附件,这次新增

所以我不能简单地“把附件数组整体覆盖掉”,而要在提交时拆成:

  • retainAttachmentIds
  • deleteAttachmentIds
  • newUploadSessionIds
  • attachmentMetas

一句话讲法:

change 页面真正难的不是上传,而是“编辑态附件差量同步”,这比单纯新增上传更接近真实业务。


八、为什么这样选型,而不是别的方案

这一段是高频追问区。

1. 为什么不用普通整文件上传

短答:

因为大文件场景下,整文件上传失败成本太高,一旦中断通常要全量重传。

展开版:

  • 大文件失败重传成本高
  • 服务端流量压力大
  • 很难支持稳定的暂停/恢复
  • 很难做分片级重试

2. 为什么不用服务端中转上传

短答:

因为那会让业务服务端承受大文件流量和带宽压力,不划算。

展开版:

  • 浏览器传给服务端
  • 服务端再传给 R2
  • 相当于文件流走了两次
  • 服务端会成为性能瓶颈

3. 为什么不用数据库存文件

短答:

数据库存结构化数据,对象存储存大文件,这是更合理的职责分离。

展开版:

  • 数据库存大文件不经济
  • 查询、备份、扩容都不友好
  • PostgreSQL 更适合存元数据和关系
  • R2 更适合存对象

4. 为什么不用现成的 tusUppy

短答:

我评估过,但当前项目需要和既有 Nitro、Neon、合同业务 CRUD 深度耦合,所以第一版选择自己实现可控性更高。

展开版:

  • 需要和既有业务模型、附件表、编辑态差量逻辑强整合
  • 需要和自定义的上传控制面接口对齐
  • 第一版更重视可解释性和可控性
  • 但后续完全可以抽象成更通用方案

九、真实边界和坑,怎么讲更像做过

这一段会让你显得不是只看过博客。

1. CORS 是真实坑

浏览器直传 R2 时,不是签了 presigned URL 就万事大吉。

还要保证 bucket CORS 允许:

  • http://localhost:8080
  • PUT
  • GET
  • HEAD
  • 以及预检请求

否则浏览器会在 OPTIONS 预检阶段就失败。

2. r2_upload_id 长度不是拍脑袋定的

一开始如果把它建成 varchar(255),是可能翻车的。
因为 R2 返回的 UploadId 实测可能更长,所以后来改成 text 更稳。

这个点你可以讲成:

这类字段不能想当然,最好按真实平台返回值和安全边界建模。

3. 不是所有数据库驱动都支持同样的事务能力

如果底层是 neon-http 这类连接方式,有些事务写法并不总是能直接按你想的那样跑通,所以实现时要看实际驱动能力,而不是只看 ORM 理想写法。

4. 业务提交和上传完成不能混为一谈

这是这套设计最关键的边界之一。

上传完成,只是对象已经落到 R2。
业务提交成功,才代表这个文件正式属于某个合同记录。

如果这两个动作绑死,很容易出现:

  • 文件上传成功了
  • 但合同主记录创建失败了
  • 最后留下无主附件

十、面试官高频追问与答法

Q1:你这个断点续传是怎么做到“不重复上传”的?

短答:

我前端会先根据文件信息生成一个 resumeFingerprint,再通过上传会话 ID 去服务端查哪些分片已经成功。后续只上传缺失分片,不会整文件重传。

展开版:

本地只负责快速找回 sessionId,真正权威状态还是服务端数据库里的 upload_sessionsupload_session_parts。所以即使页面刷新,也能和云端真实状态重新对齐。

Q2:为什么前端要做指纹,不直接拿文件名当唯一键?

短答:

因为文件名不可靠,重名很常见。

展开版:

第一版我用的是轻量级指纹,比如 file.name + file.size + lastModified + chunkSize 做 hash。它不是绝对强一致的文件内容 hash,但对当前业务场景已经足够,而且成本低。

Q3:如果某一片上传失败怎么办?

短答:

单片重试,不影响其他已成功分片。

展开版:

我把失败粒度控制在 part 级别,失败后最多重试几次,超过阈值才把整个文件状态标成 failed,让用户手动继续或重试。

Q4:刷新页面后为什么还能恢复?

短答:

因为我不是只靠内存状态,而是本地缓存加服务端会话表双保险。

展开版:

页面刷新后,前端会先通过指纹找回旧 sessionId,再调用 status 对账已上传分片。只要会话还有效,就能从中断点继续。

Q5:你怎么防止上传成功但业务提交失败的脏数据?

短答:

我把上传完成和附件物化拆成两阶段。

展开版:

upload/complete 只负责完成对象上传,不直接写正式附件表。真正写 ct_attachments 是在合同 create/update 成功时才做,这样业务数据和附件关系才是干净的。

Q6:编辑态附件为什么难?

短答:

因为不是单纯新增,而是同时有保留、删除、新增三类动作。

展开版:

所以我没有做“整包覆盖”,而是提交差量:保留哪些旧附件、删除哪些旧附件、新增哪些新附件,这样更符合真实业务,也避免误删。


十一、技术亮点怎么拔高

如果面试官问“你觉得这里最体现你能力的点是什么”,你可以从下面挑 2 到 3 个说。

1. 控制面和数据面分离

浏览器直传 R2 负责大文件数据流,Nitro + Neon 负责会话、签名、状态和最终完成动作,这样架构职责清晰,也更利于扩展。

2. 上传过程和业务提交解耦

这不是做一个孤立上传控件,而是把上传过程嵌进真实合同 CRUD 里,同时避免脏数据。

3. 共享上传状态机抽象

我没有把上传逻辑写死在单页里,而是抽成共享上传组件和状态机,让 draft-contractchange 两个页面共用。

4. 编辑态附件差量维护

change 页面不是简单数组覆盖,而是按“保留、删除、新增”做差量同步,这个设计更贴近真实企业系统。

5. 可靠性设计

方案里不仅有上传成功路径,也覆盖了失败重试、页面刷新恢复、取消上传、CORS、字段长度、平台约束这些真实边界。


十二、你怎么把它讲得像自己真的做过

不要一上来就讲一堆术语。
建议你按下面顺序讲:

第一段:先讲业务痛点

我做的是合同管理场景,附件不是玩具功能,文件大、网络波动、编辑态附件维护都是真问题。

第二段:再讲核心决策

所以我没有走普通整文件上传,而是选了浏览器直传 R2 的 multipart 方案,服务端只做控制面。

第三段:再讲关键设计

我把上传过程拆成上传会话、分片状态、最终附件物化三层,这样既能做断点续传,也能和业务 CRUD 闭环打通。

第四段:最后讲结果

最后两个页面都支持了分片上传、断点恢复、编辑态附件差量维护,而且服务端不会被大文件流量拖垮。

这四段讲顺了,面试官一般就会开始追细节。
细节部分你再从这份材料里拆回答。


十三、速记卡

如果你时间很紧,只背下面这些句子。

速记 1

我把这套方案拆成了浏览器直传 R2 的数据面,以及 Nitro + Neon 的上传控制面。

速记 2

upload_sessions 记录一次上传任务,upload_session_parts 记录哪些分片已经成功,ct_attachments 记录最终业务附件。

速记 3

断点续传的本质不是暂停继续,而是系统能知道哪些分片已经传完,并且只补传缺失分片。

速记 4

上传完成不等于业务成功,所以我把“对象上传完成”和“附件物化写库”拆成了两个阶段。

速记 5

change 页面最有技术含量的地方,不是上传本身,而是编辑态附件的差量同步。


十四、最后的练习方法

你别试图一次把全部技术细节吃透。
建议你这样练:

第 1 轮

只背熟 STAR 主讲稿,能顺着说完 3 分钟。

第 2 轮

重点理解 3 个问题:

  • 为什么不用整文件上传
  • 为什么要有上传会话和分片表
  • 为什么要把上传完成和业务提交解耦

第 3 轮

change 页面编辑态附件差量维护讲明白。

第 4 轮

对着“高频追问”练短答版,每题控制在 20 秒到 40 秒。


十五、你面试时最稳的一种说法

如果你怕紧张,就用下面这个版本开场:

我这次做的不是一个简单上传按钮,而是把大文件分片上传、断点续传和合同业务 CRUD 真正打通。核心思路是浏览器直传 Cloudflare R2,Nitro 负责上传控制面,Neon 负责记录会话和分片状态。这样既能减轻服务端压力,又能支持刷新恢复、失败重试和编辑态附件差量维护。这个项目里我觉得最有价值的点,是把上传过程数据和最终业务附件数据分开建模,避免了文件上传成功但业务提交失败带来的脏数据问题。

这段你先练熟,再慢慢往外扩。

贡献者

The avatar of contributor named as ruan-cat ruan-cat

页面历史

最近更新