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 项目背景

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

scss 复制代码
前端: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 中执行:

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

AI 生成 proposal.md

markdown 复制代码
# 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 结构),生成行为规范:

markdown 复制代码
# 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

markdown 复制代码
# Design: 订单导出功能

## 整体架构

前端导出按钮 → API 层 → Service 层 → Excel 生成 ↓ BullMQ 队列(异步模式) ↓ 完成后存储到 temp/exports/ ↓ 通知服务发送下载链接

markdown 复制代码
## 后端组件

### 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 通知 → 前端显示下载按钮

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

4.3 生成 tasks.md

markdown 复制代码
# 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

scss 复制代码
✅ 1.1 安装 exceljs 依赖
    → npm install exceljs
    → package.json 已更新

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

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

typescript 复制代码
// 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,生成归档:

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

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

markdown 复制代码
- [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 的项目:

markdown 复制代码
# 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 行以内的改动):

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

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

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

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

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

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

8.3 与代码审查结合

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

markdown 复制代码
> 继续下一个 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:

sql 复制代码
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 工作流:

markdown 复制代码
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 的最新版本。实战中的具体命令和配置可能因版本更新而略有变化。