OpenSpec 实战指南:从规范到代码的完整工作流

OpenSpec 实战指南:从规范到代码的完整工作流

发布日期:2026-06-06 | 分类:AI 开发方法论 | 难度:进阶

一、简介

本文通过一个完整的实战案例——为电商后台添加订单导出功能——带你走一遍 OpenSpec + Claude Code 的完整工作流。你将看到从需求提出到规范编写、从技术设计到代码实施、再到归档的全过程。

前置要求

  • ✅ 了解 OpenSpec 基本概念(建议先阅读 OpenSpec 基础概念与 OPSX 工作流
  • ✅ 已安装 OpenSpec CLI:npm install -g @fission-ai/openspec@latest
  • ✅ 项目已初始化:openspec init
  • ✅ 已配置 CLAUDE.md 绑定 OpenSpec 工作流

二、实战场景

2.1 项目背景

你正在开发一个电商后台管理系统,技术栈如下:

1
2
3
4
5
前端:React 18 + TypeScript + Tailwind CSS
后端:Node.js + Express + Prisma ORM
数据库:PostgreSQL
消息队列:BullMQ (Redis)
文件存储:本地文件系统

openspec/project.md 已配置好项目信息。

2.2 需求描述

用户提了一个新需求:

「订单列表页面需要加一个『导出 Excel』功能,支持按时间范围筛选导出。导出的 Excel 包含订单编号、用户昵称、商品名称、数量、单价、总金额、下单时间、订单状态。导出超过 5000 条时异步生成,完成后发通知。」

这个需求大约需要 半天到一天 的开发工作量,非常适合走完整的 OPSX 流程。


三、Step 1: /opsx:propose — 提出变更

在 Claude Code 中执行:

1
/opsx:propose "订单列表页面加导出 Excel 功能,支持按时间范围筛选,导出超过5000条时异步生成"

AI 生成 proposal.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Proposal: 订单导出功能

## 目标
为订单管理模块添加 Excel 导出功能,支持按时间范围筛选导出订单数据。

## 范围
1. 前端:导出按钮 + 时间范围选择器
2. API:`GET /api/orders/export` 接口(同步)
3. API:`POST /api/orders/export-async` + 轮询状态接口(异步)
4. 后端:Excel 生成逻辑(使用 exceljs 库)
5. 异步任务:超过 5000 条时使用 BullMQ 队列异步生成
6. 导出完成后邮件/站内通知

## 不做
- 自定义导出列(固定列)
- 导出 PDF 格式
- 调度任务(定时导出)
- 导出历史记录管理

## 工作量估算
- 后端 API + Excel 生成:约 3 小时
- 异步队列 + 通知:约 2 小时
- 前端界面:约 1.5 小时
- 测试:约 1 小时

人类审核清单 ✅

检查项 通过?
范围描述准确覆盖需求?
「不做」列表合理?
工作量估算合理? ✅ 约 7.5 小时
有没有遗漏的边界情况? ✅ 已覆盖同步/异步两种模式

四、Step 2: /opsx:explore — 探索与细化

4.1 生成 spec.md

AI 分析现有代码(订单 Model、Controller、Router 结构),生成行为规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# Spec: 订单导出功能

## API: GET /api/orders/export

- **功能:** 同步导出 Excel(≤ 5000 条时使用)
- **Query 参数:**
- `startDate` (string, ISO 8601, optional)
- `endDate` (string, ISO 8601, optional)
- **Response headers:**
- `Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
- `Content-Disposition: attachment; filename="orders_export_20260101_20260131.xlsx"`
- **Error:**
- 400: 日期格式无效
- 413: 数据超过 5000 条(提示使用异步导出)

## API: POST /api/orders/export-async

- **功能:** 提交异步导出任务(> 5000 条时使用)
- **Body:** `{ startDate?, endDate? }`
- **Response 200:** `{ taskId: string, status: "queued" }`
- **Error:** 429: 已有正在进行的导出任务(同一用户)

## API: GET /api/orders/export-async/:taskId/status

- **功能:** 查询异步导出任务状态
- **Response 200:** `{ taskId, status: "queued"|"processing"|"completed"|"failed", downloadUrl?, error? }`

## 导出 Excel 列定义(固定顺序)

| 列名 | 数据字段 | 格式 |
|------|---------|------|
| 订单编号 | order.id | 字符串 |
| 用户昵称 | order.user.nickname | 字符串 |
| 商品名称 | orderItem.productName | 字符串 |
| 数量 | orderItem.quantity | 整数 |
| 单价 | orderItem.unitPrice | 保留 2 位小数 |
| 总金额 | order.totalAmount | 保留 2 位小数 |
| 下单时间 | order.createdAt | YYYY-MM-DD HH:mm:ss |
| 订单状态 | order.status | 中文映射(pending/paid/shipped/delivered/cancelled) |

## 验证规则

- `startDate``endDate` 必须同时提供或同时省略
- 时间跨度不得超过 365 天
- `endDate` 不能早于 `startDate`

4.2 生成 design.md

1
2
3
4
# Design: 订单导出功能

## 整体架构

前端导出按钮 → API 层 → Service 层 → Excel 生成

BullMQ 队列(异步模式)

完成后存储到 temp/exports/

通知服务发送下载链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

## 后端组件

### 1. OrderExportController
- 处理路由分发(同步/异步)
- 参数校验
- 分派到对应 Service

### 2. OrderExportService
- `exportExcel(filters)`: 生成 Excel 并直接返回流
- `queueAsyncExport(filters)`: 提交异步任务
- `getExportStatus(taskId)`: 查询任务状态
- 使用 exceljs 库的 Worksheet 构建

### 3. ExportQueue (BullMQ)
- 队列名:`order-export`
- 每个用户同时只能有一个排队的导出任务
- 任务完成后保存到 `temp/exports/{taskId}.xlsx`
- 文件 1 小时后自动清理

### 4. 通知服务
- 完成时通过 WebSocket 推送到前端
- 同时记录站内通知

## 前端组件

### ExportButton
- 使用 shadcn/ui 的 Button + DatePicker
- 点击导出时根据预估数据量选择同步/异步
- 异步时显示进度条和下载链接

## 数据流

同步模式:
用户点击导出 → 前端调 GET /api/orders/export
→ OrderExportService 查询 DB → 生成 Excel → 直接下载

异步模式:
用户点击导出 → 前端调 POST /api/orders/export-async
→ 返回 taskId → 前端轮询 GET …/status
→ BullMQ worker 处理 → 生成 Excel → 存文件
→ WebSocket 通知 → 前端显示下载按钮

1
2
3
4
5
6

## 安全考量
- 导出权限仅限 admin 和 operator 角色
- 每次导出审计日志
- 导出文件 1 小时后自动删除
- 防止导出所有订单(必须指定时间范围)

4.3 生成 tasks.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Tasks: 订单导出功能

## 后端
- [ ] 1.1 安装 exceljs 依赖
- [ ] 1.2 实现 OrderExportService.exportExcel():查询 + Excel 构建
- [ ] 1.3 实现 GET /api/orders/export 同步接口
- [ ] 1.4 判断数据量 > 5000 时返回 413(提示用异步)
- [ ] 1.5 配置 BullMQ 队列与 Redis 连接
- [ ] 1.6 实现 ExportQueue worker:异步 Excel 生成
- [ ] 1.7 实现 POST /api/orders/export-async 接口
- [ ] 1.8 实现 GET /api/orders/export-async/:taskId/status 接口
- [ ] 1.9 实现文件自动清理(临时文件 1h TTL)

## 前端
- [ ] 2.1 创建 ExportButton 组件(含时间范围选择器)
- [ ] 2.2 同步导出流程:调 API → 下载文件
- [ ] 2.3 异步导出流程:提交任务 → 轮询状态 → 显示下载链接
- [ ] 2.4 WebSocket 监听导出完成通知

## 测试
- [ ] 3.1 单元测试:OrderExportService(含边界条件)
- [ ] 3.2 集成测试:同步/异步 API
- [ ] 3.3 E2E 测试:前端导出流程

人类三审清单 ✅

检查项 通过?
spec 覆盖了同步/异步两种模式?
错误边界完整(400/413/429 等)?
安全考量充分(权限、审计、自动清理)?
design 的架构方案合理? ✅ BullMQ 是标准异步队列方案
tasks 粒度适中(每个 ≤ 50 行)?
有没有遗漏的测试场景? ✅ 边界条件已覆盖

五、Step 3: /opsx:apply — 实施

5.1 实施过程示例

进入 apply 阶段后,AI 逐条执行 tasks.md:

1
2
3
4
5
6
7
✅ 1.1 安装 exceljs 依赖
→ npm install exceljs
→ package.json 已更新

✅ 1.2 实现 OrderExportService.exportExcel()
→ 创建 src/services/order-export.service.ts
→ 实现 query + Excel 构建逻辑

以下是 AI 生成的 OrderExportService 核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// src/services/order-export.service.ts
import ExcelJS from 'exceljs';
import { prisma } from '../lib/prisma';
import path from 'path';
import fs from 'fs/promises';

interface ExportFilters {
startDate?: string;
endDate?: string;
}

const COLUMN_DEFS = [
{ header: '订单编号', key: 'orderId', width: 24 },
{ header: '用户昵称', key: 'nickname', width: 20 },
{ header: '商品名称', key: 'productName', width: 30 },
{ header: '数量', key: 'quantity', width: 10 },
{ header: '单价', key: 'unitPrice', width: 12 },
{ header: '总金额', key: 'totalAmount', width: 14 },
{ header: '下单时间', key: 'createdAt', width: 22 },
{ header: '订单状态', key: 'status', width: 14 },
];

const STATUS_MAP: Record<string, string> = {
pending: '待付款',
paid: '已付款',
shipped: '已发货',
delivered: '已签收',
cancelled: '已取消',
};

export class OrderExportService {
/**
* 估算数据量(仅 COUNT,不查全量)
*/
async estimateCount(filters: ExportFilters): Promise<number> {
const where = this.buildWhereClause(filters);
return prisma.order.count({ where });
}

/**
* 查询订单数据
*/
private async fetchOrders(filters: ExportFilters) {
const where = this.buildWhereClause(filters);
return prisma.order.findMany({
where,
include: {
user: { select: { nickname: true } },
items: {
select: { productName: true, quantity: true, unitPrice: true },
},
},
orderBy: { createdAt: 'asc' },
});
}

/**
* 构建 Prisma WHERE 条件
*/
private buildWhereClause(filters: ExportFilters) {
const where: any = {};
if (filters.startDate && filters.endDate) {
where.createdAt = {
gte: new Date(filters.startDate),
lte: new Date(filters.endDate),
};
}
return where;
}

/**
* 生成 Excel 并返回 Buffer
*/
async exportExcel(filters: ExportFilters): Promise<Buffer> {
const orders = await this.fetchOrders(filters);

const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('订单导出');

// 列定义
sheet.columns = COLUMN_DEFS.map((col) => ({
header: col.header,
key: col.key,
width: col.width,
}));

// 表头样式
sheet.getRow(1).font = { bold: true, size: 11 };
sheet.getRow(1).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF4472C4' },
};
sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } };

// 数据行
for (const order of orders) {
for (const item of order.items) {
sheet.addRow({
orderId: order.id,
nickname: order.user.nickname,
productName: item.productName,
quantity: item.quantity,
unitPrice: item.unitPrice,
totalAmount: order.totalAmount,
createdAt: this.formatDate(order.createdAt),
status: STATUS_MAP[order.status] || order.status,
});
}
}

// 自动筛选
sheet.autoFilter = {
from: { row: 1, column: 1 },
to: { row: 1, column: COLUMN_DEFS.length },
};

// 冻结首行
sheet.views = [{ state: 'frozen', ySplit: 1 }];

const buffer = await workbook.xlsx.writeBuffer();
return Buffer.from(buffer);
}

private formatDate(date: Date): string {
const pad = (n: number) => n.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
}

5.2 实施过程中的常见 AI 偏差及修正

偏差 发现方式 修正方法
AI 忘记添加自动筛选(autoFilter) 复审 diff 发现 在 design.md 补充要求,重新 apply
时间范围验证写在了 Controller 而非 Service 层 代码审查 要求移动到 Service 层,保持 Controller 薄
前端导出按钮没有禁用状态 前端测试 在 spec.md 补充「导出中按钮应禁用并显示 loading」

关键原则: 发现偏差时,优先修改 spec/design 而非直接改代码。这样 AI 下次 apply 时就不会犯同样的错误。


六、Step 4: /opsx:archive — 归档

完成后执行 /opsx:archive,生成归档:

1
2
3
4
5
openspec/archive/2025Q2/order-export/
├── proposal.md ← 原始需求提案
├── spec.md ← 最终行为规范
├── design.md ← 最终技术设计
└── tasks.md ← 带完整 check 状态的实施清单

归档的 tasks.md 会保存每条任务的最终状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- [x] 1.1 安装 exceljs 依赖
- [x] 1.2 实现 OrderExportService.exportExcel()
- [x] 1.3 实现 GET /api/orders/export 同步接口
- [x] 1.4 判断数据量 > 5000 时返回 413
- [x] 1.5 配置 BullMQ 队列与 Redis 连接
- [x] 1.6 实现 ExportQueue worker
- [x] 1.7 实现 POST /api/orders/export-async 接口
- [x] 1.8 实现 GET .../status 接口
- [x] 1.9 实现临时文件自动清理
- [x] 2.1 创建 ExportButton 组件
- [x] 2.2 同步导出流程
- [x] 2.3 异步导出流程
- [x] 2.4 WebSocket 通知
- [x] 3.1 单元测试
- [x] 3.2 集成测试
- [x] 3.3 E2E 测试

七、CLAUDE.md 黄金配置模板

下面是经过验证的、完整的 CLAUDE.md 配置,适合所有使用 OpenSpec 的项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# CLAUDE.md

## 项目约定
- TypeScript + React + Tailwind CSS(前端)
- Node.js + Express + Prisma ORM(后端)
- PostgreSQL(数据库)
- 使用 Vitest 测试
- 组件用函数式 + hooks
- API 遵循 RESTful 规范

## 开发流程
1. **新功能**(> 半天):/opsx:propose → 审核 → /opsx:explore → 三审 → /opsx:apply
2. **小修复**(< 半天):直接 /opsx:apply,在描述中说明需求
3. **紧急修复**:直接改代码,事后 /opsx:archive 补流程

## 审核标准(每次必查)
- spec 必须包含所有错误边界(400/401/403/404/409/429/500)
- spec 必须包含安全考量
- design 必须说明为什么选择此方案(而非其他备选)
- tasks 粒度:每个 task 不超过 50 行代码
- 每个功能必须包含:单元测试 + 集成测试 + E2E 测试

## 代码规范
- 文件名:kebab-case(order-export.service.ts)
- 组件名:PascalCase(ExportButton.tsx)
- 接口:I 前缀(IExportFilters)
- 类型:T 前缀(TExportStatus)
- 错误处理:统一使用 AppError 类,不能在 Controller 中直接 throw Error

## OpenSpec 工作流规则
1. 所有变更必须先生成 OpenSpec 文件
2. 除非是紧急修复,否则不要跳过 proposal 阶段
3. 实施前必须通过 propose → explore 流程
4. 实施时逐条遵循 tasks.md
5. 完成后运行 archive
6. 如果发现实施偏离 spec,先更新 spec 再重新 apply

八、高级技巧

8.1 快速通道:/opsx:ff

当你需要快速修复时(比如修一个 typo 或者 3 行以内的改动):

1
/opsx:ff "修复订单详情页的金额显示少了一位小数"

效果:直接进入 apply 阶段,跳过 propose 和 explore。

8.2 增量开发:先核心后完善

对于大型功能,可以分多次 OPSX 循环:

1
2
3
第一次循环:核心导出逻辑(生成 Excel + 同步下载)
第二次循环:异步导出(队列 + 通知)
第三次循环:前端体验优化(进度条 + WebSocket)

每次循环专注于一个子目标,降低单次复杂度。

8.3 与代码审查结合

/opsx:apply 过程中,让 AI 每完成一个 task 就停下来,人类审查 diff:

1
> 继续下一个 task … 先让我看看上个 task 的 diff

这样可以尽早发现偏差,避免后面改大堆代码。

8.4 使用 archive 做回顾

定期查看 openspec/archive/,你可以获得以下价值:

  1. 变更统计: 这个季度做了哪些功能?每个功能花了多久?
  2. 模式识别: 哪些类型的 spec 经常被 AI 误解?修改 spec 的写法来避免
  3. 新人培训: 新人看 archive 就知道项目演进的完整历程

九、常见问题 (FAQ)

Q1: AI 生成的 design 不理想怎么办?

先不要急着改 design。尝试在 /opsx:explore 阶段给 AI 更多上下文,例如:

  • 「参考 UserService 的写法」
  • 「这个项目的架构模式是 repository pattern」
  • 「使用我们已有的示例模式」

如果你给的上下文足够明确,AI 生成的 design 质量会大幅提升。如果仍不理想,手动编辑 design.md 中的关键部分,然后重新执行 /opsx:apply

Q2: 实施过程中发现 spec 遗漏了某些情况怎么办?

不要直接改代码。 正确的做法是:

  1. 暂停当前 apply
  2. 执行 /opsx:explore(重新生成 spec)
  3. AI 会保留原有 spec 内容并在其基础上补充
  4. 审核更新后的 spec
  5. 重新 /opsx:apply

Q3: 同一个功能的 tasks 超过 20 条怎么办?

说明这个功能太大了,应该拆分为多个小功能。例如「订单导出功能」可以拆为:

  • order-export-core:核心导出(Excel 生成 + 同步 API)
  • order-export-async:异步导出队列
  • order-export-frontend:前端 UI

每个子功能单独走一次 OPSX 循环。

Q4: 多人协作时,每个人的 OpenSpec 文件放在哪里?

统一放在项目仓库的 openspec/ 目录下。不同人负责不同的 specs/<change-name>/。建议约定命名规范:

  • auth-forgot-password/
  • order-export-async/
  • dashboard-revenue-chart/

使用短横线连接、全小写、描述性的目录名。

Q5: 如何管理 OpenSpec 文件本身的版本?

OpenSpec 文件与代码一起 Git 管理。每次 /opsx:archive 后提交一次 commit:

1
2
git add openspec/
git commit -m "docs: archive order-export spec"

不建议将 spec 文件放在 .gitignore 中——它们是最有价值的设计文档。

Q6: 同一个功能改了多次,archive 会保留多个版本吗?

不会。当一个 change-name 被重新 archive 时,它会覆盖之前的版本。如果你想保留多个迭代版本,为每次迭代使用不同的 change name:

  • order-export-v1/
  • order-export-v2/
  • order-export-v3/

十、总结

通过本文的实战案例,你已体验了完整的 OpenSpec 工作流:

1
2
3
4
proposal → spec → design → tasks → apply → archive
| | | | | |
需求 行为 技术 实施 编码 归档
提案 规范 设计 清单 执行 记录
阶段 人类参与 AI 参与 产出物
propose 描述需求 生成提案 proposal.md
explore 三审 生成 spec/design/tasks 三个文件
apply 审查 diff 逐条实施代码 代码变更
archive - 整理归档 归档记录

核心教训:

  1. 在 spec 上花的时间,会在 debug 上省回来
  2. 发现偏差先改 spec,别直接改代码
  3. tasks 粒度要小(≤ 50 行),大了就拆功能
  4. archive 是无价的项目资产

本文基于 OpenSpec v0.x + Claude Code 的最新版本。实战中的具体命令和配置可能因版本更新而略有变化。