1. 为什么 spec.md 要有固定结构
写给人看的文档,可以随意组织,读者会自己理解。
写给 AI 用的文档,需要结构清晰、层次分明、术语一致——因为 AI 不会像人一样"猜"你想说什么,它依赖上下文的密度和结构来理解你的意图。
一份好的 spec.md 要做到两件事:
- 可读性:人能快速浏览,知道这个模块干什么
- 可执行性:AI 拿到这份文档,能直接生成符合预期的代码
2. spec.md 的标准结构
spec/
├── overview.md # 项目概述(整体级别,只写一次)
├── user/
│ ├── spec.md # 用户模块完整规格
│ └── api.md # 用户模块接口定义(可选独立)
├── order/
│ └── spec.md
├── product/
│ └── spec.md
└── shared/
├── data-model.md # 共享数据模型
├── error-codes.md # 统一错误码
└── conventions.md # 全局约定(命名、格式等)
每个模块的 spec.md 遵循同一套章节结构,AI 每次读到相同位置就能找到相同类型的信息。
3. 标准章节详解
一、概述(Overview)
写什么:这个模块是什么、解决什么问题、属于哪个业务域。
## 概述
用户模块(User Module)负责平台的账号体系管理,包括用户注册、登录、
个人信息维护、账号状态管理等核心功能。
本模块是其他所有业务模块的基础依赖,其他模块通过 userId 与用户产生关联。
避免写:技术实现细节(那是接口和数据模型章节的事)、过于宏观的废话("这个模块很重要")。
二、目标(Goals)
写什么:这个阶段要做什么,不做什么。边界要清楚。
## 目标
**本期实现:**
- 用户 CRUD 基本操作
- 邮箱 + 密码登录,JWT token 颁发
- 用户状态管理(正常 / 禁用)
- 软删除(数据不物理删除)
**本期不做:**
- 第三方登录(微信、GitHub)
- 用户角色与权限(由 RBAC 模块负责)
- 用户行为日志(由审计模块负责)
"不做什么"和"做什么"同等重要。明确范围,AI 不会自作主张帮你多加功能。
三、功能需求(Features)
写什么:按用户故事或功能点列出,每条要足够具体。
## 功能需求
### F-01 用户注册
- 支持邮箱 + 密码注册
- 注册时校验邮箱是否已存在,已存在返回 409
- 密码在存储前必须使用 bcrypt 加密(rounds=10)
- 注册成功后自动触发欢迎邮件(异步,通过消息队列)
- 注册成功返回 userId,不返回 token(需要单独登录)
### F-02 用户登录
- 支持邮箱 + 密码登录
- 密码错误返回 401,不区分"用户不存在"和"密码错误"(安全考虑)
- 登录成功返回 accessToken(有效期 2h)和 refreshToken(有效期 7d)
- 连续登录失败 5 次,锁定账号 15 分钟
### F-03 查询用户列表
- 支持按 name、email 模糊搜索
- 支持按 createdAt 排序(asc / desc)
- 支持分页:page(从1开始)+ pageSize(默认20,最大100)
- 返回数据不包含 password 字段
关键技巧:每条功能需求写到"无歧义"——AI 能直接按需求写代码,不需要再问你一遍。
四、非功能需求(Non-Functional Requirements)
写什么:性能、安全、可用性、可观测性等约束。
## 非功能需求
### 性能
- 用户查询接口 P99 响应时间 < 200ms
- 列表查询在 100 万数据量下不得全表扫描(必须加索引)
### 安全
- 密码字段禁止出现在任何接口响应中
- 所有写操作接口必须验证 JWT
- 用户只能修改自己的信息(除非有 ADMIN 角色)
### 可观测性
- 所有接口使用 Pino 记录请求日志
- 登录失败事件必须记录(userId 或 email + IP + 时间)
### 兼容性
- API 版本:v1(/api/v1/users)
- 时间字段统一使用 ISO 8601 格式(UTC)
五、接口定义(API)
写什么:每个接口的完整契约,包括路径、方法、请求参数、响应结构、错误码。
## 接口定义
### 统一响应格式
**成功:**
````json
{
"code": 0,
"message": "ok",
"data": { ... }
}
```text
**失败:**
```json
{
"code": 40001,
"message": "邮箱已存在",
"data": null
}
```text
### POST /api/v1/users — 创建用户
**请求体:**
| 字段 | 类型 | 必须 | 说明 |
| --- | --- | --- | --- |
| email | string | 是 | RFC 5322 格式 |
| password | string | 是 | 8-32位,含大小写和数字 |
| name | string | 是 | 2-50个字符 |
**成功响应(201):**
```json
{
"code": 0,
"message": "ok",
"data": {
"id": "uuid",
"email": "user@example.com",
"name": "张三",
"createdAt": "2025-01-01T00:00:00Z"
}
}
```text
**错误响应:**
| code | httpStatus | 场景 |
| --- | --- | --- |
| 40901 | 409 | 邮箱已被注册 |
| 40001 | 400 | 请求参数校验失败 |
---
### GET /api/v1/users — 用户列表
**Query 参数:**
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| page | number | 1 | 页码,从1开始 |
| pageSize | number | 20 | 每页数量,最大100 |
| search | string | - | 按 name 或 email 模糊搜索 |
| sortBy | string | createdAt | 排序字段 |
| order | asc/desc | desc | 排序方向 |
**成功响应(200):**
```json
{
"code": 0,
"message": "ok",
"data": {
"list": [...],
"total": 128,
"page": 1,
"pageSize": 20
}
}
```text
```plain
---
### 六、数据模型(Data Model)
**写什么**:数据库表结构,字段类型、约束、索引、关联关系。
```markdown
## 数据模型
### User 表
```prisma
model User {
id String @id @default(uuid())
email String @unique
password String
name String
status UserStatus @default(ACTIVE)
isDeleted Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@index([createdAt])
@@index([isDeleted, status])
}
enum UserStatus {
ACTIVE
DISABLED
}
```text
**字段说明:**
- `id`:UUID v4,由数据库生成
- `password`:存储 bcrypt hash,永远不在接口层返回
- `isDeleted`:软删除标记,所有查询默认过滤 `isDeleted = false`
- `status`:账号状态,DISABLED 状态不能登录
```plain
---
### 七、业务流程(Workflow)
**写什么**:跨步骤的流程,特别是涉及多个操作、异步处理、状态流转的场景。
```markdown
## 业务流程
### 用户注册流程
````
客户端 POST /users → 参数校验(class-validator) ↓ 失败 → 返回 400 → 查询 email 是否已存在 ↓ 已存在 → 返回 409 → bcrypt 加密密码 → 创建 User 记录 → 发布 USER_REGISTERED 事件(消息队列) → 异步:发送欢迎邮件 → 返回 userId(201)
```plain
### 用户状态流转
```
ACTIVE ←→ DISABLED
触发 ACTIVE → DISABLED:管理员手动禁用 触发 DISABLED → ACTIVE:管理员手动启用
DISABLED 状态下:
- 登录接口返回 403(账号已被禁用)
- 已有 token 在下次请求时仍有效(不主动吊销,依赖 token 过期)
```plain
```
---
### 八、验收标准(Acceptance Criteria)
**写什么**:可测试的、明确的验收条件。这部分也是单元测试的需求来源。
```markdown
## 验收标准
### 用户创建
- [x] 使用有效参数创建用户,返回 201 和 userId
- [x] 使用已存在的 email 创建,返回 409
- [x] 密码字段在响应中不存在
- [x] 数据库中密码以 bcrypt hash 形式存储
- [x] name 为空时返回 400
### 用户列表
- [x] 不传参数时返回第一页,pageSize=20
- [x] search=张 时,name 包含"张"的用户都在结果中
- [x] isDeleted=true 的用户不出现在列表中
- [x] pageSize 超过 100 时,实际返回不超过 100 条
### 安全
- [x] 未携带 token 访问需要认证的接口,返回 401
- [x] 用户 A 的 token 尝试修改用户 B 的信息,返回 403
```
---
### 九、附录(Appendix)
**写什么**:参考资料、设计决策记录、历史变更、相关文档链接。
```markdown
## 附录
### 设计决策
**为什么用软删除而不是物理删除?**
用户数据涉及业务关联(订单、评论等),物理删除会导致外键悬空或需要级联删除,
风险高。软删除保留数据完整性,后续如需数据恢复或审计也有依据。
**为什么密码错误和用户不存在返回相同错误?**
区分两种错误会让攻击者通过枚举 email 来判断账号是否存在,存在信息泄露风险。
### 相关文档
- [JWT 规范](spec/shared/auth.md)
- [统一错误码](spec/shared/error-codes.md)
- [全局分页约定](spec/shared/conventions.md)
### 变更记录
| 日期 | 变更内容 | 作者 |
| ---------- | -------------------- | ---- |
| 2025-03-01 | 初始版本 | 大伟 |
| 2025-03-15 | 增加登录失败锁定功能 | 大伟 |
```
---
## 4. 让 AI 高效读取 spec 的几个技巧
### 技巧一:用 @引用,别复制粘贴
在 Cursor 里,用 `@spec/user.md` 直接引用文件,比把内容粘贴进对话框要好。文件引用让 AI 读到完整内容,也方便你更新——spec 改了,下次对话自动生效。
### 技巧二:关键约定重复写
如果某个约定非常重要(比如统一响应格式),在 shared/conventions.md 写一遍,在每个模块 spec 的接口章节也写一遍。冗余比遗漏强。
### 技巧三:负向说明比正向说明更有效
```markdown
# 差
密码要安全存储
# 好
密码必须用 bcrypt 加密(rounds=10),
禁止以明文或可逆加密方式存储,
禁止在任何接口响应中返回 password 字段
```
### 技巧四:Spec 是活文档,不是一次性的
功能做完之后,spec 不要扔。下次扩展这个模块,先更新 spec,再让 AI 按新 spec 生成。spec 同时也是你的项目记忆。
---
## 5. 一份 spec.md 的完整模板
````markdown
# [模块名] 规格文档
**版本**:v1.0
**最后更新**:2025-xx-xx
**负责人**:大伟
---
## 一、概述
[模块描述,2-4句话]
## 二、目标
## **本期实现:**
## **本期不做:**
## 三、功能需求
### F-01 [功能名]
-
## 四、非功能需求
### 性能
### 安全
### 可观测性
## 五、接口定义
### 统一响应格式
### [接口1]
### [接口2]
## 六、数据模型
````prisma
// Prisma schema
```text
## 七、业务流程
```plain
[流程图,用缩进和箭头表示]
```text
## 八、验收标准
- [ ]
- [ ]
## 九、附录
### 设计决策
### 相关文档
### 变更记录
```plain
---
## 6. 小结
一份好的 spec.md,是你和 AI 协作的"共同语言"。
它不需要写得像企业级 PRD 那样庞大繁琐,但要做到:
- 结构固定,AI 每次都知道去哪里找什么信息
- 表述无歧义,功能、字段、格式都有明确定义
- 边界清晰,本期做什么、不做什么都写明白
花两小时写好一份 spec,能让你在接下来几天的 AI 协作中少踩几十个坑。这个时间投入,是值得的。
```text