1.1 需求拆解:上传、下载、秒传、分享

场景:一个会说人话的需求,才好估排期

需求来源很具体。我们团队五个人,设计稿、演示视频、打包好的安装包,天天在群里扔。微信群文件有大小限制,邮件附件更小,用公共网盘要么限速要么要会员。有一天产品同学问我,咱不是会写代码吗,自己搭一个呗?

我随口答了一句行,然后就开始后悔——因为上传两个字背后藏着四种完全不同的活:

  • 小文件上传:截图、文档,几 MB,传完就能看,要求就是快,三秒内不出结果就有人催。
  • 大文件上传:几个 G 的视频,传到一半断了要不要重传?能不能接着断的地方继续?
  • 秒传:同一份安装包第二个人再传一次,能不能直接说已有,别再费一遍流量和存储?
  • 分享:传完之后,怎么给一个不在群里的人?他要不要注册账号?要不要装客户端?

如果需求只写支持上传下载,这四件事每一件都能把开发周期拖长一倍。而且需求一旦模糊,开发就会自由发挥:有人给上传加了服务端压缩,有人给分享加了权限树,有人顺手做了个相册,最后做出来一个谁都不会用的怪物。所以我第一步不是建项目,而是拉着产品坐下来,把需求拆成可量化的条目,没有指标的需求一律打回。这本书里反复出现的所有设计,追根溯源都来自这张需求表。除了列出要做什么,我们还专门列了一张不做清单,把多端同步、在线编辑、版本历史这些听着合理但当下没人力做的功能写进去并钉死,谁提需求先看这张表。

需求表还有一个隐藏好处:它让排期变得可估。以前估排期靠拍脑袋,现在每个功能有了指标,就能拆出具体动作——分片上传要做几个分片、并发几路、失败重试几次,每一条都能落到工时上。产品同学拿着这张表也能自己判断优先级,哪些指标可以放宽,哪些是底线,争吵少了一大半。

方案:把需求拆成三张表

功能需求我拆成四块,每块给出硬指标。指标的作用是堵住争论,比如断点续传要不要做,一看表里写着必须做,就不用开会讨论了:

功能 硬指标 为什么这么定
上传 单文件最大 5GB,支持分片与断点续传 视频素材常有 2GB 以上,断网重传会让人疯
下载 私有读,签名 URL 有效期 15 分钟 文件不公开,但分享对象不能复杂到不会用
秒传 按内容指纹去重,命中即返回旧地址 安装包反复传,既费流量又费存储
分享 短链加提取码,有效期可配置 链接要能贴进微信,不能一长串参数

非功能需求同样得量化,否则上线后全是糊涂账。团队成员随时可能加人,存储容量也只会涨,这些都要在第一天就留好余量:

维度 目标 落地手段
并发 50 人同时上传不卡 客户端分片并发,后端只发密钥不接数据
容量 起步 100GB,可平滑扩到 TB 不自建,直接用 COS 按量扩
安全 数据不经后端、密钥不过夜 临时密钥加最小权限
成本 月成本两位数以内 存储用标准类型,下载走 CDN 回源
可用性 上传成功率 99% 以上 失败分片自动重试,全局超时兜底

每条需求还要配一个验收方式,不然验收会上又是凭感觉打分。举个例子,断点续传如果没有验收方式,上线时谁也说不清它到底灵不灵,只能等用户出事了才知道。这张表后来成了我们对账的依据,也是 QA 同事最爱的文档:

需求 验收方式 负责人
断点续传 传到一半杀进程,重启后继续且不重复上传 前端
秒传 两人传同一文件,第二人一秒内返回成功 后端
分享 微信里打开短链,输提取码即下载,无需登录 前后端
签名下载 链接 16 分钟后打开必然失败 后端

配置:先把 API 定下来

需求拆完,先不写存储,把后端 API 草图定下来。因为后端只做协调,所以接口极少,一个文件中转站的全部后端入口,其实一只手数得过来:

// 1. 换取上传凭证:客户端报告文件名、大小、指纹,后端返回临时密钥与上传地址
POST /api/upload/token

// 2. 秒传检查:指纹命中则直接返回已有文件信息,客户端跳过上传
POST /api/upload/check

// 3. 分片状态:查询已上传分片编号,用于断点续传对齐进度
GET /api/upload/parts?uploadId=xxx

// 4. 创建分享:生成长链,再由短链服务换成短码
POST /api/share/create

// 5. 换取下载地址:返回带签名的 COS 或 CDN 地址
GET /api/download/url?key=xxx

五个接口,对应上传、秒传、断点续传、分享、下载五件事,后端只管发凭证,所有复杂逻辑都在客户端和云端跑。文件落库的元数据也精简到最小,只存协调必需的字段:

// File 是后端记账用的元数据,不包含文件内容
type File struct {
    ID      string    // 对象键
    Size    int64     // 字节数
    Hash    string    // 内容指纹,秒传依据
    Owner   string    // 上传者
    Created time.Time // 入库时间
}

这份 API 表后来改过两版,但骨架没动过——需求清楚,接口就清楚。改的两版分别是把秒传检查独立成接口、以及把分片状态查询从完成接口里拆出来,两次都是因为功能耦合导致客户端逻辑混乱,拆完立刻清爽。关于这层协调架构的来龙去脉,可以看 1.3 架构草图:后端只做协调层,后端为什么能瘦身到这个程度,那篇里有完整推演。上传的具体协议细节在 2.1 直传设计、2.3 大文件分片与断点续传 两篇里逐层展开。

踩坑:三个让我返工的决定

第一个坑,没有定文件大小上限。初版我只想着支持大文件,没设上限,结果有人传了个 20GB 的虚拟机镜像,分片上传跑了四十分钟,浏览器内存差点炸掉。后来老老实实加上 5GB 上限,并对超过 500MB 的文件在前端做二次确认。需求的另一面是约束,不设上限的需求不是需求,是许愿。同样被忽略的还有并发上限:五十个人同时开五个分片,就是二百五十条连接,客户端不做节流,最先扛不住的是用户的路由器。

第二个坑,秒传指纹一开始用的是文件名加大小。同一份设计稿改个名就当新文件,去重完全失效,存了一堆字节完全相同的副本,存储费白交。正确做法是算内容指纹:小文件整体算哈希,大文件用头部数据加文件大小做粗判,命中后再全量校验。指纹怎么算、算哪一段,是秒传成败的关键,细节在 3.1 秒传一篇里展开。

第三个坑,分享功能设计得太早。我一开始就把分享接口和上传接口耦合在一起,上传完成自动生成链接。后来发现有些文件只想自己存着,不想被分享,又只能加参数回滚,数据库里多出一堆没意义的分享记录。做需求时要记住,上传是上传,分享是分享,两件事,存储和传播必须解耦。

第三个半坑,验收方式写得太晚。需求上线前才补验收标准,结果断点续传的测试没人会做——原来没有人定义什么叫传到一半,是杀进程还是断网还是关浏览器。验收方式必须和指标同时诞生,否则指标就是一句口号。后来这张验收表变成 QA 的测试用例,每个版本回归一遍,省下了大量扯皮时间。

需求文档我们维护成活文档,每加一个功能必须同时更新验收标准。有次评审会上有人质疑某个指标无法验证,当场翻文档发现验收一栏写着约等于,从此禁止模糊措辞。可验证才叫需求,不可验证的只能叫愿望。

确立需求阶段我们还做了一件对的事:找了五个未来用户做原型访谈,拿着线框图一个一个过操作。访谈推翻了我们两个自以为重要的功能,加了一个我们自己没想到的需求。用户嘴上说的和手上做的经常不是一回事,观察操作比听反馈更可靠。

需求拆解做到最后是一种直觉训练:看到一个功能一句话,脑子里自动浮现它要哪些接口、存什么表、怎么验收。这种直觉没有捷径,全靠一个个需求踩过来。

小结

需求拆解这事听起来虚,但它是后面所有设计的依据。三张表加五个接口,就是我们这个网盘的全部需求规格。砍掉一切不在表里的功能,是我在这个项目里做得最正确的决定之一。后来有同事想加文件夹、加评论、加版本历史,我都让他们先回去把表填了——填不进指标的功能,大概率是伪需求。需求表还顺带解决了另一个问题:谁来测试。表里每条都有验收方式和负责人,上线前一天拉个清单逐条过,过了才发版,不过就延期,谁也没话说。

还有一个经验要补在这里:需求表不是一次性的产物,它要跟着项目活。每加一个功能,先更新表;每砍一个功能,也更新表。我们的这张表后来贴在团队 wiki 首页,比任何文档都管用,新来的同事看一遍就能知道这个系统想干什么、不想干什么。动手写第一章代码之前,把表钉在墙上,能省掉后面无数次的返工。这件事的成本只有一下午,收益却贯穿了整个项目的每一天。

延伸阅读