返回笔记首页

第 15 集:spec.md 结构设计(AI 看得懂的需求文档)

主题配置

1. 为什么 spec.md 要有固定结构

写给人看的文档,可以随意组织,读者会自己理解。

写给 AI 用的文档,需要结构清晰、层次分明、术语一致——因为 AI 不会像人一样"猜"你想说什么,它依赖上下文的密度和结构来理解你的意图。

一份好的 spec.md 要做到两件事:

  • 可读性:人能快速浏览,知道这个模块干什么
  • 可执行性:AI 拿到这份文档,能直接生成符合预期的代码

2. spec.md 的标准结构

plain
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)

写什么:这个模块是什么、解决什么问题、属于哪个业务域。

markdown
## 概述

用户模块(User Module)负责平台的账号体系管理,包括用户注册、登录、
个人信息维护、账号状态管理等核心功能。

本模块是其他所有业务模块的基础依赖,其他模块通过 userId 与用户产生关联。

避免写:技术实现细节(那是接口和数据模型章节的事)、过于宏观的废话("这个模块很重要")。


二、目标(Goals)

写什么:这个阶段要做什么,不做什么。边界要清楚。

markdown
## 目标

**本期实现:**

- 用户 CRUD 基本操作
- 邮箱 + 密码登录,JWT token 颁发
- 用户状态管理(正常 / 禁用)
- 软删除(数据不物理删除)

**本期不做:**

- 第三方登录(微信、GitHub)
- 用户角色与权限(由 RBAC 模块负责)
- 用户行为日志(由审计模块负责)

"不做什么"和"做什么"同等重要。明确范围,AI 不会自作主张帮你多加功能。


三、功能需求(Features)

写什么:按用户故事或功能点列出,每条要足够具体。

markdown
## 功能需求

### 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)

写什么:性能、安全、可用性、可观测性等约束。

markdown
## 非功能需求

### 性能

- 用户查询接口 P99 响应时间 < 200ms
- 列表查询在 100 万数据量下不得全表扫描(必须加索引)

### 安全

- 密码字段禁止出现在任何接口响应中
- 所有写操作接口必须验证 JWT
- 用户只能修改自己的信息(除非有 ADMIN 角色)

### 可观测性

- 所有接口使用 Pino 记录请求日志
- 登录失败事件必须记录(userId 或 email + IP + 时间)

### 兼容性

- API 版本:v1(/api/v1/users)
- 时间字段统一使用 ISO 8601 格式(UTC)

五、接口定义(API)

写什么:每个接口的完整契约,包括路径、方法、请求参数、响应结构、错误码。

markdown
## 接口定义

### 统一响应格式

**成功:**

````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
## 业务流程

### 用户注册流程
````
text

客户端 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
text