Spec Kit 实战 04 · 个人博客增量需求实战:定时发布
预计阅读 10 分钟

Spec Kit 实战 04 · 个人博客增量需求实战:定时发布

上一章:图书管理系统实战 | 返回索引 | 下一章:老项目接入 Spec Kit

阅读指南 主案例走完了从 0 到 1,这一章换 brownfield 场景:仓库里已经有代码和既定行为,新需求只能做加法。命令说明不再重复,只盯老项目特有的坑——兼容边界、状态机、双 worker 幂等。结尾附一场纸面对照:这次增量若交给 OpenSpec 管理,会是什么形态。

示例项目是一个 Spring Boot 单体博客服务(Java 8、MyBatis-Plus、MySQL、xxl-job 定时任务),分层与响应约定和主案例一致:controller/request/vo 分层、ResultDTO 统一响应、CodeEnum 错误码。它比主案例小得多,但内部写法是同一套。真到博客需要对外提供 RPC 或拆发布入口那天,再按 api/service/web/publish 拆模块不迟。

1. 从现有事实开始

当前场景与目标

博客服务已经支持草稿、预览和立即发布。现在要增加“定时发布”,同时保持公开 URL、权限和立即发布路径不变。

开始前的项目状态

TEXT
1personal-blog/
2├── src/main/java/com/example/blog/
3│   ├── BlogStarter.java                # @SpringBootApplication 启动类
4│   ├── controller/PostController.java  # 含 publishNow 等接口
5│   ├── request/PostSaveRequest.java
6│   ├── vo/PostVO.java
7│   ├── service/PostService.java
8│   ├── service/impl/PostServiceImpl.java   # createDraft / publishNow / preview
9│   ├── mapper/PostMapper.java
10│   ├── entity/PostEntity.java
11│   └── job/                            # 已有 xxl-job 任务处理器
12├── src/main/resources/
13│   ├── application.yml + application-local.yml
14│   ├── mapper/PostMapper.xml
15│   └── db/ddl/                         # 现有表结构脚本
16├── src/test/java/com/example/blog/
17│   ├── PostServiceTest.java
18│   └── JobTest.java
19└── pom.xml

先建立事实清单:

TEXT
1现有状态:draft -> published(PostStatusEnum)
2现有入口:POST /api/post/publishNow
3现有公开详情:GET /api/post/queryBySlug(对外渲染为 /posts/{slug})
4现有调度:xxl-job 控制台注册的每分钟任务
5现有时间存储:数据库 DATETIME,统一 UTC 口径
6必须回归:立即发布、草稿预览、公开 URL、作者权限

【社区实践】官方 Existing Projects 指南建议先从有界变化开始,不要把“为整个旧系统补规格”当作第一个 feature。

2. 描述增量需求并控制范围

错误输入

TEXT
1给博客增加高级发布功能,顺便优化文章模块。

问题:没有目标行为、兼容边界和非目标;“高级”“优化”会让 agent 自由扩张。

可执行输入

TEXT
1/speckit.specify
2在现有个人博客服务中增加定时发布:作者可以为 draft 文章设置一个未来发布时间;
3到点后系统通过现有 xxl-job 任务自动公开文章。
4
5必须保持:现有立即发布接口、草稿预览、作者权限、文章 slug 和公开 URL 不变。
6定时发布后,文章内容和 URL 与立即发布结果一致。
7
8第一版不做周期发布、社交平台分发、发布失败通知、内容审批流、批量调度,
9也不允许用户选择展示时区。不要重构整个 posts 模块或引入新队列。

增量需求编号

ID需求
BFR-001仅 draft 文章可设置未来发布时间
BFR-002API 接收 ISO 8601 offset 时间并转为 UTC 保存
BFR-003scheduled 文章在到点前不可公开访问,但作者仍可预览
BFR-004作者可改期或取消,取消后回到 draft
BFR-005到点后通过现有 xxl-job 任务只发布一次
BFR-006停机错过时间后,下一次任务运行应补发
BFR-007两个 worker 同时扫描时只有一个完成状态迁移
BFR-008立即发布、权限、slug 和公开 URL 行为保持不变

边界条件与验收标准

【AI 产出,人工确认】spec.md 应包含:

Markdown
1### User Story 1 - Schedule a draft (P1)
21. Given 文章为 draft,When 作者设置未来时间,
3   Then 状态变为 scheduled,持久化 UTC 时间,到点前公开接口返回 404。
42. Given 时间等于或早于服务端当前时间,When 设置调度,
5   Then 返回 SCHEDULE_TIME_MUST_BE_FUTURE,文章保持 draft。
63. Given 文章已 published,When 设置调度,
7   Then 返回 POST_NOT_DRAFT,公开文章不变。
8
9### User Story 2 - Reschedule or cancel (P1)
101. Given 文章为 scheduled,When 改为另一个未来时间,Then 只有新时间生效。
112. Given 文章为 scheduled,When 取消,Then 回到 draft 且 scheduled_at 清空。
12
13### User Story 3 - Publish due posts (P1)
141. Given scheduled_at <= now,When 任务运行,Then 原子变为 published 且只执行一次。
152. Given 系统停机期间错过时间,When 恢复后任务首次运行,Then 文章被补发。
163. Given 两个 worker 同时扫描,When 都尝试发布,Then 只有一个状态更新成功。
17
18### Regression Acceptance
19- 立即发布仍使用原接口并保持响应契约。
20- draft/scheduled 的作者预览仍可用。
21- slug 和公开 URL 不改变。
22- 非作者仍不能修改调度。

开发者需要人工确认

  • 到点条件是 scheduled_at <= now
  • 过去时间拒绝,而不是立即发布;
  • 取消回到 draft,不删除文章;
  • 公开访问在到点前返回现有系统规定的 404(公开详情查询本就过滤非 published 文章),不发明新状态码;
  • “不引入新队列”是本 feature 的技术边界,若现有 xxl-job 不满足才回 plan 讨论。

执行后的项目状态

TEXT
1specs/002-scheduled-publishing/spec.md

3. 用 Clarify 补齐旧系统相关语义

完整提示词

TEXT
1/speckit.clarify
2重点检查:过去时间、时区输入、改期/取消、文章编辑与调度的关系、
3停机补发、双 worker 幂等、立即发布与定时发布冲突。优先询问会改变
4现有状态机、数据迁移或外部 API 的问题。

澄清决策

问题【人工决策】答案影响
调度后还能编辑内容吗?可以;发布时读取最新已保存内容BFR-003、测试
scheduled 文章点立即发布?允许;原子转 published 并清空 scheduled_atBFR-008、状态机
API 时区格式?必须含 offset;服务端转 UTCBFR-002、接口字段
两 worker 如何判定成功?条件更新 status=scheduled AND scheduled_at<=nowBFR-007、Plan
发布动作失败怎么办?保持 scheduled,下一轮重试;本期不发通知BFR-005、Out of Scope

【人工确认】这些答案必须回写 spec.md。如果只留在对话里,新会话和评审者无法恢复上下文。

4. Plan:优先复用,不另起架构

完整提示词

TEXT
1/speckit.plan
2先读取 com/example/blog/service/impl/PostServiceImpl.java、
3controller/PostController.java、job/ 下现有任务处理器、PostMapper.xml、
4现有 DDL 脚本和测试。复用现有 xxl-job 调度和 ResultDTO/CodeEnum 响应约定,
5不引入 Redis、消息队列或第二套 PostService。
6
7为 t_post 增加 SCHEDULED 状态枚举值和 nullable 的 scheduled_at(DATETIME,
8UTC 口径);提供向前与回滚 DDL。增加设置、改期、取消调度的接口,沿用现有
9request/vo 风格。发布任务每分钟查找到期记录,用条件 UPDATE 从 scheduled
10原子迁移到 published;失败记录保持 scheduled,下一轮重试。
11立即发布 scheduled 文章时清空 scheduled_at,并保持现有 URL 与权限。
12
13输出真实文件路径、状态机、接口契约、DDL、双 worker/停机补发测试,
14以及全部现有 posts 回归命令(注意父 POM 可能默认 skipTests=true)。
15任何新依赖都必须说明现有能力为何不足。

状态机

MERMAID
1stateDiagram-v2
2    [*] --> draft
3    draft --> published: 立即发布
4    draft --> scheduled: 设置未来时间
5    scheduled --> scheduled: 改期
6    scheduled --> draft: 取消调度
7    scheduled --> published: 到点任务或立即发布

计划中的最小数据变化

TEXT
1t_post.status: PostStatusEnum 增加 SCHEDULED(tinyint 新枚举值,不动旧值)
2t_post.scheduled_at: DATETIME, nullable, UTC 口径
3index: idx_status_scheduled_at(status, scheduled_at) 支持到期扫描

落到 BT001 交付的 V042__schedule_post.sql

SQL
1ALTER TABLE `t_post`
2    ADD COLUMN `scheduled_at` DATETIME NULL COMMENT '定时发布时间(UTC 口径)',
3    MODIFY COLUMN `status` TINYINT NOT NULL
4        COMMENT '0=draft,1=scheduled,2=published';
5ALTER TABLE `t_post`
6    ADD INDEX `idx_status_scheduled_at` (`status`, `scheduled_at`);

两处细节值得多看一眼。SCHEDULED 用新值 1,不动 0 和 2 的既有含义,存量数据一行都不用迁移;索引建在 (status, scheduled_at) 上,因为发布任务的扫描条件永远是 status = 1 AND scheduled_at <= now。回滚脚本 U042__schedule_post_rollback.sql 反向执行 DROP INDEX、DROP COLUMN——但先想清楚再跑,生产数据里已经落了 scheduled_at 的文章回滚就丢了调度。

API 契约示例

TEXT
1POST /api/post/schedule        PostScheduleRequest { postId, scheduledAt }
2POST /api/post/cancelSchedule  @RequestParam postId
3POST /api/post/publishNow      # 原接口,兼容 scheduled 文章

【人工决策】路径必须符合当前项目既有路由风格;示例不是 Spec Kit 官方规定。若原项目使用别的动作命名(如 /updateStatus),应沿用原模式。

防止平行架构

评审 plan 时搜索:

Bash
1rg -n "queue|redis|rocketmq|kafka|ScheduledPostService|第二个调度" specs/002-scheduled-publishing

若出现未经批准的新基础设施,要求 agent 回答:现有 xxl-job 哪条已验证限制使它不能满足 BFR-005..007?没有证据就删除该方案。

5. Tasks:从“大任务”拆成可验证增量

错误任务

Markdown
1- [ ] 实现定时发布功能

它没有 ID、文件路径、需求映射、依赖或验证方法。

修订后的任务

Markdown
1## Phase 1: Migration and state
2- [ ] BT001 添加 SCHEDULED 状态值与 scheduled_at 列的向前 DDL src/main/resources/db/ddl/V042__schedule_post.sql
3- [ ] BT002 添加回滚 DDL 并验证执行/回滚 src/main/resources/db/ddl/U042__schedule_post_rollback.sql
4- [ ] BT003 [P] 更新 PostEntity、PostStatusEnum 与索引映射 src/main/java/com/example/blog/entity/PostEntity.java
5
6## Phase 2: Schedule management (BFR-001..004)
7- [ ] BT004 [P] 编写未来时间、过去时间、非 draft、offset 校验测试 src/test/java/com/example/blog/PostSchedulingTest.java
8- [ ] BT005 实现设置/改期/取消调度 PostServiceImpl.schedule/cancelSchedule
9- [ ] BT006 实现 /api/post/schedule 与 /api/post/cancelSchedule controller/PostController.java
10- [ ] BT007 验证 scheduled 到点前公开 404、作者预览可用 src/test/java/com/example/blog/PostSchedulingTest.java
11
12## Phase 3: Due publishing (BFR-005..007)
13- [ ] BT008 [P] 编写停机补发与失败重试测试 src/test/java/com/example/blog/PublishDuePostsJobTest.java
14- [ ] BT009 编写双 worker 原子状态迁移测试 src/test/java/com/example/blog/PublishDuePostsJobTest.java
15- [ ] BT010 实现并注册 publishDuePosts 任务 job/PublishDuePostsJob.java + PostMapper.xml 条件 UPDATE
16
17## Phase 4: Compatibility (BFR-008)
18- [ ] BT011 更新立即发布以支持 scheduled 并清空 scheduled_at service/impl/PostServiceImpl.java
19- [ ] BT012 运行立即发布、预览、权限、slug、公开 URL 回归测试
20- [ ] BT013 执行 quickstart、DDL 回滚与完整测试(-DskipTests=false)

重新验证任务

运行 /speckit.analyze 后,检查:

  • BFR-001..008 是否都有任务;
  • BT004 与 BT007、BT008 与 BT009 修改同一文件,不能标成彼此并行;
  • BT010 依赖 BT001 和 BT009;
  • BT011 必须在 BT012 之前;
  • 没有周期发布、通知、审批或新队列任务。

6. 需求变化:不要直接改 Tasks

开发中产品提出:“定时发布前必须由编辑审核。”这不是 BT014 的小补充,它引入角色、审批状态、权限和失败路径。

先分类变化

变化处理
调整错误文案,不改变契约更新任务/代码,保留证据
scheduled_at <= now 改为 < now修改 spec,再同步 plan/tasks/tests
增加发布审批新 feature,保持当前 Out of Scope
发现 xxl-job 无法多实例幂等更新 research/plan,再调整 tasks

控制需求膨胀的回复模板

TEXT
1“发布审批”改变了角色、权限和状态机,不属于已批准的 BFR-001..008。
2当前 feature 继续交付定时发布;另建下一 feature 描述审批用户故事、状态迁移、
3权限和验收。两者通过现有 published 状态衔接,不在本 PR 顺手实现。

新建还是修改现有规格

【官方能力】Spec Kit 不强制一种维护策略:

  • Flow-forward:新建 003-publish-approval,最适合审计清晰的团队;
  • Living spec:先改现有 spec,再生成/修订下游工件;
  • Flow-back:发现可以从任意工件进入,但必须重新对齐所有工件。

选择由团队写入 Constitution 或贡献指南。

7. Implement 与回归验收

分阶段提示词

TEXT
1/speckit.implement
2只执行 BT001-BT003。运行 DDL 执行/回滚和实体相关测试,不修改发布逻辑。
TEXT
1/speckit.implement
2执行 BT004-BT007。沿用现有 controller/request/vo 和 ResultDTO 错误响应模式;
3运行 PostSchedulingTest 和原有 preview 测试。不要实现后台发布任务。
TEXT
1/speckit.implement
2执行 BT008-BT013。双 worker 测试必须验证只有一次状态迁移;运行完整
3posts/job 测试、现有立即发布/预览/权限/URL 回归和 DDL 回滚。报告未执行项。

【AI 产出】到点发布任务的核心代码可能类似(xxl-job 1.x 处理器风格):

Java
1@Lazy
2@Component
3@JobHander("publishDuePostsJob")
4public class PublishDuePostsJob extends IJobHandler {
5
6    @Resource
7    private PostMapper postMapper;
8
9    @Override
10    public ReturnT<String> execute(String... strings) throws Exception {
11        // 条件 UPDATE 把“到期”与“迁移”绑在同一条 SQL 上:
12        // status 仍是 SCHEDULED 才会被更新,天然幂等,双 worker 并发时后者影响 0 行
13        int published = postMapper.publishDuePosts(new Date());
14        XxlJobLogger.log("[publishDuePostsJob] --- 本轮定时发布文章数: [{0}]", published);
15        return ReturnT.SUCCESS;
16    }
17}
XML
1<update id="publishDuePosts">
2    UPDATE `personal-blog`.`t_post`
3    SET `status` = 2,
4        `scheduled_at` = NULL,
5        `update_time` = now()
6    WHERE `status` = 1
7      AND `scheduled_at` &lt;= #{now}
8      AND `is_delete` = 0
9</update>

(枚举值以 PostStatusEnum 实际定义为准:此处假设 DRAFT=0、SCHEDULED=1、PUBLISHED=2。)

设置调度的校验片段沿用 Request 注解 + service 业务检查的分层:

Java
1private void validateScheduleTimeOrThrow(PostEntity post, Date scheduledAt) {
2    if (PostStatusEnum.PUBLISHED.equals(post.getStatus())) {
3        throw new FailedException(CodeEnum.POST_NOT_DRAFT.getCode());
4    }
5    if (!scheduledAt.after(new Date())) {
6        throw new FailedException(CodeEnum.SCHEDULE_TIME_MUST_BE_FUTURE.getCode());
7    }
8}

人工审查至少要问:停机补发依赖 scheduled_at <= now 的宽松条件,确认没有场景把“未来但已错过审批”的文章误发;publishNow 与任务并发时状态迁移是否仍然单向;任务失败重试会不会重复发布。

验收矩阵

验收需求任务
仅 draft 可设置未来时间BFR-001、BFR-002BT004-BT006
到点前不公开但可预览BFR-003BT007
改期与取消BFR-004BT004-BT006
到点一次发布、失败重试BFR-005BT008、BT010
停机补发BFR-006BT008、BT010
双 worker 幂等BFR-007BT009、BT010
旧能力兼容BFR-008BT011-BT013

执行后状态

TEXT
1personal-blog/
2├── specs/002-scheduled-publishing/
3├── src/main/resources/db/ddl/V042__schedule_post.sql
4├── src/main/java/com/example/blog/
5│   ├── controller/PostController.java            # 新增 schedule/cancelSchedule
6│   ├── request/PostScheduleRequest.java
7│   ├── service/impl/PostServiceImpl.java         # schedule/cancelSchedule/publishNow 调整
8│   ├── job/PublishDuePostsJob.java
9│   └── enums/PostStatusEnum.java                 # 增加 SCHEDULED
10├── src/main/resources/mapper/PostMapper.xml      # publishDuePosts 条件 UPDATE
11└── src/test/java/com/example/blog/
12    ├── PostSchedulingTest.java
13    └── PublishDuePostsJobTest.java

完成检查清单

  • BFR-001..008 与 BT001..013 可双向追溯。
  • 立即发布、预览、权限、slug 和 URL 回归通过。
  • 没有引入新队列、第二套调度或 PostService。
  • 停机补发、失败重试、双 worker 是真实测试。
  • 新提出的审批需求没有混入当前 feature。
  • /speckit.converge 最终为 Converged;这仍不替代部署验收。

8. 对照:这次增量用 OpenSpec 管理会是什么样

博客此刻还没引入 OpenSpec(第 5 章试点收尾接入,并在同一章跑第一个日常增量),这里做一次纸面对照,看清两种工作流在"同一次增量"上的形态差异。定时发布在 OpenSpec 中会是一个变更提案:

TEXT
1openspec/changes/add-scheduled-publishing/
2├── proposal.md        # Intent:作者希望设定未来时间自动公开;
3│                      # Scope:不含周期发布/通知/审批/时区选择
4├── specs/post-publishing/spec.md
5│                      # delta:## Purpose + ## ADDED Requirements
6│                      # (BFR-001..008 的行为版,Scenario 用 GIVEN/WHEN/THEN)
7├── design.md          # 复用 xxl-job、条件 UPDATE 状态迁移(本章 Plan 的关键决策)
8└── tasks.md           # BT001..BT013 的分组编号复选清单

对照着看本章做过的事:/speckit.specify 生成的 spec.md 对应 delta 里的 ADDED Requirements;Clarify 的五个澄清决策会写进 delta 的 Scenario 边界;/speckit.plan 的技术边界落到 design.md;BT 任务表换成分组编号清单。

差异在顺序与归宿。Spec Kit 先为增量建 feature 目录、生成全套工件,验收之后目录定格成历史快照;OpenSpec 从现行规格出发只写差异,落地归档时 delta 合并回 specs,规格永远只有一份现行版。至于收敛需求的手法——BFR 编号、兼容边界、变更分类——两条路线上完全复用。

第 6 章的三种维护模型讲的就是这两条路线的取舍;第 5 章会在博客上把 OpenSpec 的循环真正跑一遍。

下一章:老项目接入 Spec Kit

继续阅读

推荐阅读

教程2026年9月9日Spec Kit 中文实战指南(系列导读)系列导读:这套资料写给准备把 Spec Kit 用进真实项目的开发者。图书管理系统走完整流程;个人博客定时发布解释增量变更;老项目接入章再用文章标签筛选做小范围试点。教程2026年9月9日Spec Kit 实战 01 · 认识 Spec Kit:从一个开发问题开始Spec Kit 实战系列第 1/8 章:先看产物如何流动、命令在哪里运行。教程2026年9月9日Spec Kit 实战 02 · 安装与项目初始化Spec Kit 实战系列第 2/8 章:安装 v1.0.5,分别初始化新项目和已有项目。
Spec Kit 实战 04 · 个人博客增量需求实战:定时发布 | 博击长空