需求文档写到什么程度才算合格?一份能直接开工的PRD模板
「文档我发你了,你看下,没什么问题就开工吧。」
我打开那份文档,一共十二行,是一份功能名称列表。
一、我收到过最离谱的一份需求文档
那是去年接的一个售后工单系统。产品在飞书上给我发来一段话,标题叫「工单系统需求」,正文是这样的:
- 工单创建
- 工单列表
- 工单分配
- 工单处理
- 工单统计
十二行,五个功能点,没有一个字提到字段、状态、权限。
我问他工单有哪些状态,他说「就正常的状态啊」。我问正常是什么,他说「你看着设计就行,你是专业的」。
我当时还真就看着设计了。我按自己的理解做了五个状态、三个角色,两星期做完上线。然后验收会上,运营问了六个问题,我一个都答不上来:
- 客户三天不回复,工单能自动关吗?
- 处理人离职了,他的工单怎么办?
- 两个人同时点「抢单」,算谁的?
- 工单关了之后客户又来找,是重开还是新建?
- 超时没处理的工单,谁会收到通知?
- 导出来的统计报表,按创建时间算还是按关闭时间算?
那次验收会开了两个小时,最后结论是「先按现在的用,有问题再说」。我心里清楚,这句话的意思是后面有得改。
果然,后面两个月我改了三轮:第一轮加自动关闭,第二轮加离职转派,第三轮加并发抢单的锁。三轮加起来又是十几天。如果这六件事在开工前被写进文档,那十几天根本不会存在。
事后我反复想这件事,发现问题不在产品不靠谱,而在我们俩对"需求文档"这四个字的理解完全不一样。他觉得需求文档是"我要什么东西"的清单,我觉得需求文档是"怎么做"的依据。中间差的那部分,就是我后来自己瞎猜、又猜错的那部分。
所以这篇文章我想讲清楚一件事:一份需求文档写到什么程度,才算能开工。
二、先定义"能开工":三条判据,五条硬标准
我现在判断一份需求文档合不合格,就看三件事:
落到实处,我总结成五条硬标准:
| 字段有定义 | 每个字段的类型、长度、是否必填、枚举值 | 前后端理解不一致,联调吵架 |
| 状态有流转 | 状态清单 + 允许的路径 + 不可逆的终态 | 状态判断散落在代码里,加一个状态改一周 |
| 异常有分支 | 每个操作失败、超时、并发时的处理规则 | 线上出问题时临时拍脑袋 |
| 边界有数值 | 分页上限、导出上限、超时时间、最大长度 | 慢 SQL 拖垮库,或者 OOM |
| 权限有矩阵 | 角色 × 操作 的允许/禁止表 | 越权操作,出事无法追责 |
这五条里只要缺一条,这份文档在我这儿就是"草稿",不是"文档"。
三、一份能直接开工的 PRD 模板
下面这份模板是我现在用的,拿售后工单系统做例子。你可以直接抄走改,我逐段说为什么每一段都不能省。
文档名称:售后工单系统需求文档
版本:v1.2
状态:待评审
## 0. 修订记录
| 版本 | 日期 | 修改人 | 修改内容 | 影响范围 |
|—|—|—|—|—|
| v1.0 | 2026-03-01 | 产品 | 初稿 | 全部 |
| v1.2 | 2026-03-08 | 我 | 补充异常流程与数据字典 | 第4、5节 |
## 1. 背景与目标
客服团队目前用群聊 + Excel 跟进售后问题,工单易丢失、无法统计响应时长。
本期目标:工单全流程线上化,可追踪、可统计 SLA 达成率。
不解决的问题(明确不做):不对接电话客服系统、不做客户自助门户、不做智能派单算法。
## 2. 名词表
| 名词 | 定义 |
|—|—|
| 工单 | 一次客户售后请求的完整记录 |
| SLA | 从工单创建到首次响应/解决的时间承诺 |
| 重开 | 已关闭工单因客户再次反馈而回到处理中 |
## 3. 角色与权限矩阵
| 操作 | 客服 | 客服主管 | 客户 |
|—|—|—|—|
| 创建工单 | ✓ | ✓ | ✓ |
| 抢单/认领 | ✓ | ✓ | ✗ |
| 转派他人 | 仅本人名下 | ✓ | ✗ |
| 强制关闭 | ✗ | ✓ | ✗ |
| 重开 | ✗ | ✓ | ✓(限本人工单,7天内) |
## 4. 状态与流转
状态清单:待分配 / 处理中 / 待客户回复 / 已解决 / 已关闭
| 当前状态 | 可流转到 | 触发条件 | 操作者 |
|—|—|—|—|
| 待分配 | 处理中 | 抢单或指派 | 客服、主管 |
| 处理中 | 待客户回复 | 回复客户并挂起 | 处理人 |
| 待客户回复 | 处理中 / 已关闭 | 客户回复 / 超 72 小时未回复自动关闭 | 客户、系统 |
| 处理中 | 已解决 | 提交解决方案 | 处理人 |
| 已解决 | 已关闭 / 处理中 | 客户确认 / 客户不认可 | 客户 |
| 已关闭 | 处理中 | 7 天内客户重开 | 客户 |
不可逆终态:已关闭超过 7 天后不可再重开,只能新建工单。
## 5. 功能需求(每个功能含六要素)
### 5.1 工单创建
– 描述:客户提交售后请求,生成工单
– 前置条件:客户已登录
– 正常流程:填写表单 → 校验 → 生成工单号 → 进入待分配 → 通知客服组
– 异常流程:
– E1 表单校验失败:逐字段返回错误,保留已填内容
– E2 同一客户 5 分钟内提交相同标题:拦截并提示已有工单号(幂等键:客户ID + 标题MD5)
– E3 附件上传失败:工单仍创建成功,附件标记为上传失败,允许重试
– 边界与约束:标题 2-100 字;描述最多 2000 字;附件最多 5 个,单个 ≤ 10MB
– 验收标准:按 E1/E2/E3 各一条用例,正常流程一条用例
### 5.2 工单抢单
– 描述:客服从未分配工单中认领一条
– 正常流程:点击认领 → 校验工单仍为待分配 → 写入处理人 → 状态置为处理中
– 异常流程:
– E1 并发抢单:以数据库乐观锁 version 为准,后到者返回「已被认领」
– E2 工单已被抢:提示并刷新列表
– 边界与约束:单客服同时处理中工单上限 20 条,超出禁止认领
– 验收标准:并发 10 个请求抢同一工单,有且仅有 1 个成功
(其余功能同结构,略)
## 6. 数据字典
| 字段 | 类型 | 必填 | 说明 | 约束 |
|—|—|—|—|—|
| ticket_no | VARCHAR(32) | 是 | 工单号 | 唯一,规则 GD+年月日+6位序列 |
| title | VARCHAR(100) | 是 | 标题 | 2-100 字 |
| priority | TINYINT | 是 | 优先级 | 0-P0 / 1-P1 / 2-P2 / 3-P3,默认 2 |
| status | VARCHAR(20) | 是 | 状态 | 见第 4 节枚举 |
| assignee_id | BIGINT | 否 | 处理人 | 待分配时为空 |
| sla_deadline | DATETIME | 是 | 首次响应截止时间 | 创建时间 + 优先级对应时长 |
| closed_time | DATETIME | 否 | 关闭时间 | 关闭时写入 |
## 7. 非功能性需求
– 性能:工单列表分页查询响应时间不高于 500ms(单表数据量 50 万以内)
– 并发:抢单接口需保证同一工单仅一人成功
– 审计:状态变更、转派、强制关闭全量写操作日志
– 数据保留:工单数据保留 3 年,日志保留 1 年
## 8. 验收用例清单
| 用例编号 | 场景 | 预期结果 |
|—|—|—|
| TC-001 | 客户提交标题为空 | 提示"标题不能为空",不生成工单 |
| TC-002 | 两个客服同时抢单 | 1 成功 1 失败,失败方提示已被认领 |
| TC-003 | 客户 72 小时未回复 | 工单自动关闭,状态为已关闭 |
## 9. 待确认项
| 问题 | 负责人 | 状态 |
|—|—|—|
| P0 工单的 SLA 时长是多少 | 运营 | 待确认 |
| 处理人离职后工单是否自动释放 | HR 系统对接人 | 已确认:自动转派主管 |
这份模板里,有几段是我后来加上的,也是最容易被忽略的:
第 0 节修订记录。 看起来最没用,实际救过我一次。需求改到第三版的时候,前端拿着 v1.0 的截图来问我为什么和实现对不上,我翻修订记录两分钟就说清了。
第 1 节里的"不做清单"。 这是最容易被砍掉、也最该写的一段。需求文档只写要做什么,等于默认"没写的都要做"。明确写清楚本期不做什么,能挡掉一半的临时加需求。
第 5 节的六要素。 描述、前置条件、正常流程、异常流程、边界与约束、验收标准——六个缺一不可。我以前写需求只写前三个,结果异常处理和边界全靠开发自己猜。
第 6 节数据字典。 这是 Java 后端最该盯紧的一节,也是我最容易跟产品吵起来的一节。因为它要求对方给出确定的类型和取值,而产品往往觉得"这个你们自己定就行"。我的做法是自己先写一版,再拿去让他确认,比反过来问效率高得多。
第 9 节待确认项。 很多人觉得文档里留待确认项是丢人的事,恰恰相反。把"我不知道"明明白白写出来,比让它藏在正文里、等开发到一半才暴露出来强一百倍。我的经验是,一份合格的需求文档,待确认项通常在三到八条之间,一条都没有反而说明写得太粗。
这套模板用下来,一份中等规模模块的需求文档大概六到十页。听起来不少,但这里面有将近一半是表格,真正要写的句子没几句,填表的时间远少于后面改代码的时间。
四、五种最常见的不合格需求文档
我这些年见过的需求文档,不合格的方式出奇地一致,基本逃不出下面五种。
4.1 只有正常流程,没有异常流程
这是最普遍的一种。文档里写着"用户提交工单,系统生成工单",写得很顺。但提交失败呢?重复提交呢?附件传一半断了呢?
我的处理办法是给每个操作强制配异常编号。E1、E2、E3 一路编下去,编不出来就说明这个地方没想清楚。上面模板里 5.1 那节就是这么写的。
这个习惯还有个副作用是好的:异常流程写完,测试用例也就写完了,几乎是 1:1 对应。
还有一个容易被忽略的点:异常流程要写清楚"系统应该做什么",而不只是"提示失败"。「上传失败请重试」是给用户看的提示,不是给开发看的规则。真正要写的是"上传失败时工单仍然创建成功,附件标记为失败状态并允许重试"——也就是失败之后数据处于什么状态、还能不能补救。
4.2 只有功能名,没有数据字典
“工单要有优先级”——这是功能名。“优先级是 TINYINT,取值 0/1/2/3 分别对应 P0/P1/P2/P3,默认 2”——这是数据字典。
数据字典缺失的后果是前后端各写各的。前端以为优先级返回字符串,后端存的是数字;前端以为时间是时间戳,后端返回的是格式化字符串。等联调才发现,改起来要动两层。
数据字典我一般直接写成建表语句,让文档和代码一一对应:
CREATE TABLE `ticket` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`ticket_no` VARCHAR(32) NOT NULL COMMENT '工单号 GD+yyyymmdd+6位序列',
`title` VARCHAR(100) NOT NULL COMMENT '标题 2-100字',
`description` VARCHAR(2000) DEFAULT NULL COMMENT '问题描述',
`customer_id` BIGINT NOT NULL COMMENT '客户ID',
`priority` TINYINT NOT NULL DEFAULT 2 COMMENT '优先级 0-P0 1-P1 2-P2 3-P3',
`status` VARCHAR(20) NOT NULL COMMENT '状态 见状态机定义',
`assignee_id` BIGINT DEFAULT NULL COMMENT '处理人,待分配时为空',
`sla_deadline` DATETIME NOT NULL COMMENT '首次响应截止时间',
`closed_time` DATETIME DEFAULT NULL COMMENT '关闭时间',
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ticket_no` (`ticket_no`),
KEY `idx_status_assignee` (`status`, `assignee_id`),
KEY `idx_customer_create` (`customer_id`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='售后工单表';
注意 version 那个字段。它不是从"功能"里推出来的,是从"两个人同时抢单怎么办"这个异常流程里推出来的。异常流程会反向决定表结构,这也是为什么异常流程不能省。
4.3 只有"支持XX",没有边界数值
“支持导出”——导出多少条?导出的过程中超时了怎么办?导出的文件多久过期?
“支持批量操作”——一次最多多少条?中途失败是整体回滚还是跳过失败的?
我在文档里现在但凡看到"支持",就在后面跟一个括号写数值。上面模板里写的是:标题 2-100 字、附件最多 5 个单个 ≤ 10MB、单人处理中工单上限 20 条、客户 72 小时未回复自动关闭。这些数字有一半是我拍的,但拍出来的数字也比没有数字强,因为拍的数字可以讨论、可以改,空白只能靠猜。
4.4 没有权限矩阵
"谁能操作"这种事,写在正文里一定会漏。必须做成矩阵,行是操作、列是角色,每个格子只能是允许或禁止。
我上面那份模板里的矩阵一共五行三列,十五个格子。写的时候你会发现有些格子你自己都不知道答案——比如"客服能不能转派别人的工单"。这种格子就是待确认项,去找人问,别自己填。
4.5 没有验收标准,也没有"本期不做"
这两件事其实是同一件事的反面:一个是"怎样算做完",一个是"哪些不算这期的活"。
没有验收标准的项目,验收会上就会变成各说各话。我现在写验收标准的原则是必须可测——「响应要快」不合格,「列表查询响应时间不高于 500ms」才合格。
五、用 /需求分析 生成之后,我必补的六个地方
现在我接到需求,会先让 /需求分析 出一份初稿,它会在项目 docs 目录下生成需求文档和业务设计文档。这一步上一篇讲过,不重复。
这里我想说的是:初稿到能开工之间,永远差着一段人工补齐的距离。 我列一下每次必补的东西,以及为什么 AI 补不了。
| 异常流程 | 覆盖常见失败场景,偏通用 | 业务特有的失败分支(离职转派、重复投诉) | 它不知道你们公司的特殊情况 |
| 数据字典 | 类型和长度基本合理 | 精度、枚举取值、唯一键规则 | 精度跟业务单价/量级强相关 |
| 权限矩阵 | 角色划分清晰,操作粒度偏粗 | 逐级拆到按钮级,补上"谁能转派别人的" | 组织架构只有你知道 |
| 边界数值 | 多数不写,或写得很保守 | 分页上限、导出上限、超时时长、并发上限 | 数值要跟运维和历史数据对齐 |
| 非功能需求 | 有性能、安全等条目 | 换成可测的具体数值和口径 | "响应快"没法验收 |
| 不做清单 | 基本不写 | 本期明确不做的范围 | 只有你知道排期砍了谁 |
补齐这些大概占我整个需求分析时间的三成,另外七成是思考和找人确认。这个比例我觉得挺健康——AI 负责把骨架搭出来并且不遗漏通用项,我负责往里填只有我知道的东西。
我举个具体的例子。/需求分析 给工单系统生成的初稿里,权限部分只写了「客服、客服主管、客户」三个角色,每个角色一句话描述。这不够用,因为"客服能不能转派别人的工单"这种格子是空的。我把它拆成操作 × 角色的矩阵,十五个格子,其中三个我当场答不上来,第二天找主管确认后才填上。这三个格子要是没被摊开摆到表格里,就会变成我脑子里的一个默认值,然后在某天变成一个生产事故。
还有一个我每次都会补的地方:跟已有系统的衔接。AI 生成的文档是站在"从零开始"的视角写的,但真实项目从来不是从零开始。工单系统要读客户信息,那客户数据在哪个库?工单通知要发到企业微信,那现有的消息中心接口是什么?这些衔接点不写进文档,开发到一半就会卡住去问人。
补齐之后还有一步:把最终文档回喂给后续环节。我执行 /前后端设计 的时候会明确指向这份改过的文档路径,这样生成的数据库设计和接口设计才是基于确认后的结论,而不是基于初稿。这一步不做,前面全白改。
六、开工前的十分钟自检
文档写完,我不急着建表,会花十分钟过一遍下面这份清单。有任何一条答不上来,就回去补文档,不写代码。
第 8 条是我最看重的。文档写完我会假设自己是个不了解业务的人重读一遍,凡是读着要停下来想的地方,就是要补的地方。
补完之后,把文档里的约束翻译成代码层的校验,让规则在编译和运行期都能拦住:
@Data
public class TicketCreateDTO {
@NotBlank(message = "标题不能为空")
@Size(min = 2, max = 100, message = "标题长度为 2-100 字")
private String title;
@Size(max = 2000, message = "问题描述最多 2000 字")
private String description;
@NotNull(message = "客户ID不能为空")
private Long customerId;
@NotNull(message = "优先级不能为空")
@Min(value = 0, message = "优先级取值非法")
@Max(value = 3, message = "优先级取值非法")
private Integer priority;
@Size(max = 5, message = "附件最多 5 个")
private List<String> attachments;
}
这段东西没有任何技术含量,但它存在的意义是:需求文档里那条"标题 2-100 字"不再只是一行字,谁违反它都会在接口层被拦下来。文档和代码对得上,返工才不会发生。
七、写在最后
我以前特别抗拒写文档,觉得那是给领导看的、跟写代码没关系的事。后来被返工搞怕了才慢慢接受一个现实:文档不是写给别人的,是写给三个月后的你自己的——那个版本的你已经忘了当时为什么这么设计,只能靠文档回忆。
AI 出来之后,写文档这件事的门槛低了很多。一份结构完整、覆盖了通用项的需求文档初稿,现在几分钟就能拿到。但这也带来一个新的风险:太容易拿到一份"看起来很完整"的文档,以至于你忘了检查它缺什么。
我现在的做法是拿到初稿先挑刺,按那五条硬标准一条一条过,把缺的东西补齐,再往下走。多花的那点时间,换回来的是后面不用返工。
说句扎心的实话:大部分人不是不会写需求文档,是从来没被人要求过——所以也就从来没人告诉过他,他写的那份不合格。
下一篇回到实操:拿到这份能开工的文档之后,怎么把它变成数据库设计和接口设计。





