上一章:安装与项目初始化 | 返回索引 | 下一章:个人博客增量需求实战
阅读指南 这是全书的主案例。从已经初始化的
library-system/出发,完整走过 Constitution、Specify、Clarify、Plan、Checklist、Tasks、Analyze、Implement、测试验收、Converge 和规格回溯,最后把交付的能力登记进 OpenSpec 的现行能力目录。章节多、工件多,但每一节都在回答同一个问题——怎么让“做完”有据可查。读的时候重点盯三样东西:错误码、并发语义、任务的文件路径。
【AI 产出】以下工件和代码是连贯示例,不是 GitHub 官方 golden output,也没有在本仓库实际运行成一个应用。 【人工决策】示例技术栈采用 Java 8、Spring Boot 2、MyBatis-Plus 3.4、MySQL、JUnit + MockMvc 与 Maven 多模块(api/service/web)的工程形态;Spec Kit 不默认推荐这些技术。
0. 固定案例契约
后续所有文档使用同一组标识,避免 Spec、Tasks 和测试各叫一套名字。
需求编号
| ID | 需求 |
|---|---|
| FR-001 | 管理员录入图书,ISBN、书名、作者必填,ISBN 唯一,库存为非负整数 |
| FR-002 | 管理员编辑书名、作者和总库存;总库存不得小于当前借出数 |
| FR-003 | 按书名、作者或完整/部分 ISBN 搜索,返回总库存与可借数量 |
| FR-004 | 管理员为有效会员创建 14 天借阅记录 |
| FR-005 | 借书与库存扣减原子完成;并发竞争最后一本时只有一个请求成功 |
| FR-006 | 同一会员、同一书目最多一条 active loan |
| FR-007 | 归还只能成功一次,并原子增加可借数量 |
| FR-008 | 借阅状态为 ACTIVE、OVERDUE 或 RETURNED;due_at == as_of 仍为 ACTIVE |
| FR-009 | 会员可查看自己的借阅;管理员可查看指定时刻的全部逾期借阅 |
| FR-010 | 只有 LIBRARIAN 可录入/编辑图书及办理借还;MEMBER 只能查询图书和本人借阅 |
| FR-011 | 校验失败和冲突返回稳定错误码,不暴露内部异常 |
| FR-012 | 应用上下文提供现有用户身份;登录、注册和角色配置不在本期范围 |
成功标准
| ID | 标准 |
|---|---|
| SC-001 | P1 的图书维护、查询、角色边界、借书和还书场景都有自动化测试 |
| SC-002 | 两个请求竞争最后一本时恰好一个成功,库存不为负 |
| SC-003 | 新开发者按 quickstart.md 可在 10 分钟内完成一次借还验证 |
接口契约
本案例的接口惯例:控制器挂在 api/资源名 前缀下,动作用 /create、/updateById、/queryPage 这类动词后缀,查询分页一律 POST + 请求体:
| 方法与路径 | 目的 | 关联需求 |
|---|---|---|
POST /api/book/create | 录入图书 | FR-001、FR-010、FR-011 |
POST /api/book/updateById | 编辑图书与库存 | FR-002、FR-010、FR-011 |
POST /api/book/queryPage | 搜索和查看库存 | FR-003、FR-010 |
POST /api/loan/create | 管理员办理借书 | FR-004、FR-005、FR-006、FR-010、FR-011 |
POST /api/loan/returnById | 管理员办理还书 | FR-007、FR-010、FR-011 |
POST /api/loan/queryMemberPage | 查看本人借阅及计算状态 | FR-008、FR-009、FR-010 |
POST /api/loan/queryOverduePage | 管理员查看逾期 | FR-008、FR-009、FR-010 |
1. Constitution:先约束不可接受的实现
当前场景与目标
项目刚初始化,只有 Spec Kit 基础设施。团队需要先规定库存一致性、测试、依赖和错误边界,否则 agent 可能用"读库存再减一"的非原子逻辑,或者无理由引入队列和缓存。
开工前先把开发链摆正:需求简报和 Backlog 收敛业务判断,再调用 Spec Kit 展开规格、计划、任务和实现。简报和 Backlog 是团队自定义资产,不是 Spec Kit 的固定文件。
开始前的项目状态
1library-system/
2├── .specify/
3│ └── memory/constitution.md
4└── .agents/skills/先整理需求简报和 Backlog
这两份文档不来自任何 Spec Kit 命令,是团队自己动笔写的,也是整条流水线的源头。做法很朴素:把“做一个图书管理系统”背后的模糊想法摊开,收进一页简报;暂时不做又不甘心丢掉的想法,全部放进 Backlog,别让它们悄悄混进本期范围。
先建目录和两个空文件(内容在下面),随写随提交:
1library-system/
2└── docs/requirements/
3 ├── library-mvp-brief.md # 需求简报:一页纸的产品判断
4 └── backlog.md # Backlog:暂不做的事项登记【团队起草,AI 可代拟后人工修订】简报写成这样(内容怎么来的:产品把原始诉求聊清楚,工程把角色和边界补上):
1# 图书管理 MVP 需求简报
2
3## 背景
4内部图书角目前靠一张共享表格登记借还:谁借了哪本、是否逾期,
5全靠人翻表格核对。本期做一个最小的管理系统替代它。
6
7## 本期目标
81. 管理员能录入和编辑图书(书名、作者、ISBN、库存)。
92. 管理员能按书名、作者或 ISBN 查书,看到库存。
103. 管理员能办理借书和还书,借期 14 天。
114. 会员能查看自己的借阅和逾期状态。
12
13## 角色
14- LIBRARIAN(图书管理员):维护书目、办理借还、查逾期。
15- MEMBER(普通会员):查书、看本人借阅。
16- 身份由应用上下文提供;登录、注册本期不做。
17
18## 明确不做
19细粒度权限、预约、罚款、采购、多分馆、通知。
20
21## 未知项(先记下,留给 Clarify 回答)
22- 库存按书目聚合还是按实体副本管理?
23- ISBN 采用什么校验规则?
24- 编辑总库存时,当前借出数怎么约束?
25- 逾期边界:到期当天算不算逾期?Backlog 是一张登记表,每行注明来源,谁提议的就能追溯:
1# Backlog
2
3| 想法 | 来源 | 状态 |
4|---|---|---|
5| 预约已借出的图书 | 产品提议 | 未排期 |
6| 逾期罚款 | 产品提议 | 未排期 |
7| 实体副本条码管理 | 工程提议 | 未排期 |
8| 到期提醒通知 | 产品提议 | 未排期 |
9| 登录与角色配置 | 架构决定 | 另立 feature |
10
11以上均未进入 001-library-circulation。将来启动时每项单独立项,
12不回头扩本期范围。两份文件的作用要分清:简报写产品判断(做什么、为谁做、不做什么),不写技术方案,那是 plan 的事;简报里的“未知项”也不用现在回答,它们就是下一阶段 Clarify 的提问清单。这一步做完的预期结果:两个文件已提交,后续 /speckit.specify 的输入整段取自简报(见第 2 节)。
使用的命令与完整输入
1/speckit.constitution
2为图书管理系统制定项目原则:
31. 借书、还书和库存更新必须在单个数据库事务中完成;任何路径都不得让
4 available_copies 小于 0 或大于 total_copies。
52. 业务规则必须有自动化测试,至少覆盖正常借还、无库存、重复借阅、
6 重复归还和最后一本的并发竞争。
73. API 错误必须返回稳定、机器可读的错误码,不暴露内部堆栈和 SQL。
84. 优先使用 JDK、Spring Boot 与父 POM 已管理的依赖;新增依赖必须在
9 plan.md 记录理由与替代方案。
105. 每个 feature 必须提供可重复执行的 quickstart、DDL 脚本与回滚说明。
116. 规格、计划、任务和实现必须一起评审;AI 生成内容不得自行批准。生成的文件及关键内容
【官方能力】更新 .specify/memory/constitution.md。
【AI 产出】关键原则示例:
1# Library System Constitution
2
3## I. Transactional Circulation
4借书、还书与库存更新 MUST 在一个事务中完成(rollbackFor = Exception.class)。
5数据库与条件更新 MUST 共同保证:0 <= available_copies <= total_copies。
6
7## II. Testable Business Rules
8每条借还规则 MUST 有可执行测试;并发测试不得用两个顺序请求冒充。
9
10## III. Stable External Errors
11冲突 MUST 映射为 CodeEnum 稳定错误码;内部异常、SQL 和堆栈不得进入响应。
12
13## IV. Minimal Dependencies
14新增依赖 MUST 在 plan.md 记录 Decision、Rationale、Alternatives。
15
16## Governance
17Constitution 高于 feature plan。修改需人工评审、版本说明和迁移影响。开发者需要人工确认
- 这些原则是否是团队愿意长期执行的项目规则,而不是本功能临时要求;
- 目标 MySQL 的隔离级别(默认 REPEATABLE READ)下条件更新的行为是否满足库存语义,本地与生产版本差异要写进 research;
- "测试先行"是严格 TDD,还是只要求实现合并前有测试,不要让文本产生假承诺。
执行后的项目状态
1docs/requirements/library-mvp-brief.md # 团队起草,已提交
2docs/requirements/backlog.md # 已提交,含五项未排期想法
3.specify/memory/constitution.md # 已由团队审查常见错误、原因和修正
- 错误:"代码高质量、性能优秀。"原因:无法检查。修正:写库存不变量、稳定错误码和具体测试边界。
- 错误: 把"借期 14 天"写进 constitution。原因:它是业务需求,不是全项目原则。修正:放入
spec.md。 - 错误: 每个 feature 都重写 constitution。原因:治理规则失去稳定性。修正:仅在项目原则变化时更新。
本阶段完成检查清单
- 原则可通过代码、测试或评审动作检查。
- 没有把特性需求误写成全局原则。
- 团队已人工批准,而不是接受 AI 的自评结果。
2. Specify:把需求写成可独立验收的故事
当前场景与目标
Constitution 已建立,但"图书管理系统"仍太宽。现在只定义 what/why、用户故事、边界、功能需求和成功标准,不决定框架或数据库。
开始前的项目状态
1.specify/memory/constitution.md # 已批准
2specs/ # 尚无本 feature使用的命令与完整输入
1/speckit.specify
2根据 docs/requirements/library-mvp-brief.md 创建图书管理 MVP 的规格。
3
4角色:LIBRARIAN 和 MEMBER。身份由应用上下文提供,本期不实现登录、注册和角色配置。
5
6P1:LIBRARIAN 可以录入和编辑图书。ISBN、书名、作者必填;ISBN 唯一;
7库存为非负整数;总库存不能改到小于当前借出数。
8P1:LIBRARIAN 和 MEMBER 都可以按书名、作者或完整/部分 ISBN 查询图书,
9并看到总库存与可借数量。
10P1:LIBRARIAN 为有效会员办理借书和归还。借期是 borrowed_at 后 14 天;
11最后一本的并发借阅只允许一个成功;同一会员不能同时借同一书目两次;
12重复归还不能再次增加库存。
13P1:MEMBER 可以查看自己的借阅及 ACTIVE、OVERDUE、RETURNED 状态,不能查看他人记录。
14P2:LIBRARIAN 可以查看指定时刻所有逾期且未归还的借阅。
15
16成功标准:P1 的图书维护、查询、角色边界和借还场景有自动化测试;
17两个请求竞争最后一本时只允许一个成功;新开发者能按 quickstart
18在 10 分钟内完成录书、查询、借书、查看状态和归还。
19
20不做细粒度权限配置、实体副本条码、预约、罚款、采购、多分馆和通知。
21请只描述用户目标、业务规则、边界和可测结果,不选择技术栈。生成的文件及关键内容
目录编号由仓库状态决定,此处假设是:
1specs/001-library-circulation/
2├── spec.md
3└── checklists/requirements.md【AI 产出】spec.md 应至少包含:
1# Feature Specification: Library Circulation MVP
2
3## User Story 1 - Maintain catalog (Priority: P1)
4LIBRARIAN 录入和编辑图书。
5
6**Acceptance Scenarios**:
71. Given 合法 ISBN、非空书名/作者、total_copies=3,When 录入,Then 返回新图书且 available_copies=3。
82. Given ISBN 已存在,When 再次录入,Then 返回 ISBN_ALREADY_EXISTS。
93. Given 当前借出 2 本,When 把 total_copies 改为 1,Then 返回 TOTAL_BELOW_BORROWED,库存不变。
104. Given MEMBER 身份,When 录入或编辑图书,Then 返回 FORBIDDEN。
11
12## User Story 2 - Search books (Priority: P1)
13LIBRARIAN 和 MEMBER 按书名、作者或 ISBN 查询,并看到总库存和可借数。
14
15**Independent Test**: 录入三本图书,按部分书名和部分 ISBN 查询;无匹配时返回空列表。
16
17## User Story 3 - Borrow and return (Priority: P1)
181. Given 有库存且会员无相同 active loan,When LIBRARIAN 办理借书,
19 Then due_at=borrowed_at+14 days、available_copies 减 1。
202. Given available_copies=0,When 借书,Then 返回 BOOK_UNAVAILABLE。
213. Given 相同 active loan,When 再借,Then 返回 ACTIVE_LOAN_EXISTS。
224. Given 借阅未归还,When 归还,Then returned_at 被记录且库存加 1。
235. Given 借阅已归还,When 再次归还,Then 返回 LOAN_ALREADY_RETURNED 且库存不变。
24
25## User Story 4 - View loan status (Priority: P1)
26MEMBER 只能查看自己的借阅。as_of 时刻下:returned_at 非空为 RETURNED;
27未归还且 due_at < as_of 为 OVERDUE;其余为 ACTIVE。
28
29## User Story 5 - View all overdue loans (Priority: P2)
30LIBRARIAN 查看指定时刻全部 OVERDUE 借阅;MEMBER 请求该列表返回 FORBIDDEN。
31
32## Functional Requirements
33- FR-001: LIBRARIAN 可录入合法图书,ISBN、书名、作者必填,ISBN 唯一。
34- FR-002: LIBRARIAN 可编辑书名、作者和库存,总库存不得小于当前借出数。
35- FR-003: LIBRARIAN 和 MEMBER 可按书名、作者或 ISBN 搜索并看到库存。
36- FR-004: LIBRARIAN 可为有效会员创建借期 14 天的借阅。
37- FR-005: 借书与库存扣减原子完成,最后一本并发竞争恰好一个成功。
38- FR-006: 同一会员、同一书目最多一条 active loan。
39- FR-007: 归还只成功一次并原子增加库存。
40- FR-008: 系统按 as_of 派生 ACTIVE、OVERDUE、RETURNED 状态。
41- FR-009: MEMBER 只能查看本人借阅,LIBRARIAN 可查看全部逾期。
42- FR-010: 只有 LIBRARIAN 可维护图书和办理借还。
43- FR-011: 校验失败和冲突返回稳定错误码,不暴露内部异常。
44- FR-012: 身份由受信任应用上下文提供,本期不实现登录、注册和角色配置。
45
46## Success Criteria
47- SC-001: P1 的图书维护、查询、角色边界、借书和还书场景都有自动化测试。
48- SC-002: 两个请求竞争最后一本时恰好一个成功,库存不为负。
49- SC-003: 新开发者按 quickstart 可在 10 分钟内完成录书、查询、借书、查看状态和归还。
50
51## Out of Scope
52登录、注册、细粒度权限配置、实体副本条码、预约、罚款、采购、多分馆、通知。spec 里的 ISBN_ALREADY_EXISTS、BOOK_UNAVAILABLE 等语义名是业务层契约名。实现时它们落到 CodeEnum 的七位数字错误码上,映射关系在 Plan 阶段固定(见第 4 节)。
开发者需要人工确认
- P1 是否真的能独立交付价值;
LIBRARIAN/MEMBER权限是否足够,身份从哪里进入应用;- ISBN 校验采用什么业务规则,编辑总库存时怎样计算当前借出数;
SC-003的"10 分钟"从什么环境和什么起点测量;- 错误码是外部契约还是仅示例;一旦确认,后续 plan/tasks/tests 必须一致;
- out-of-scope 是否覆盖最可能膨胀的功能。
执行后的项目状态
1specs/001-library-circulation/spec.md
2specs/001-library-circulation/checklists/requirements.md
3.specify/feature.json # 指向当前 feature,通常机器本地状态常见错误、原因和修正
- 错误: 在 spec 写"使用 MyBatis-Plus/Redis"。原因:把 how 混进 what。修正:移到 plan。
- 错误: 只有正常借还。原因:AI 会猜重复和并发语义。修正:写成 Given/When/Then。
- 错误: 所有故事都是 P1。原因:没有可裁剪 MVP。修正:逾期列表为 P2,借还和搜索为 P1。
需求简报和 SPEC 是两层东西。简报保留目标、范围和未知项,是产品判断的载体;SPEC 再把它们写成用户故事、规则和验收场景,是开发与验收之间的约定。会议纪要或简报不能原样替代 spec.md,反过来也一样。
本阶段完成检查清单
- FR-001..FR-012 各自可验证且没有技术实现词替代业务结果。
- 每个 P1 故事可以独立测试和演示。
- SC-001..SC-003 有明确测量方式。
- Out of Scope 已由产品和工程共同确认。
3. Clarify:在数据模型形成前消除歧义
当前场景与目标
Spec 看似完整,但库存粒度、ISBN 校验、身份来源、总库存编辑和 14 天的算法都会改变 Plan。
开始前的项目状态
spec.md 有用户故事和 FR,但尚未进入技术设计。
使用的命令与完整输入
1/speckit.clarify
2重点检查库存模型、ISBN 与字段校验、LIBRARIAN/MEMBER 的身份来源和权限、
3总库存编辑、14 天的时间语义、同书重复借阅、最后一本并发竞争、
4重复归还和借阅状态边界。只问会改变需求、接口或数据模型的问题。AI 提问与人工回答
【官方能力】一次运行最多提出 5 个针对性问题;需要时可再次运行并换关注域。
| 问题 | 【人工决策】答案 | 回写位置 |
|---|---|---|
| 库存按实体副本还是书目聚合? | MVP 用 total_copies/available_copies 聚合,不建副本条码 | FR-001、FR-002、Assumptions |
| ISBN 和文本怎样校验? | ISBN 去连字符后校验 ISBN-10/13;书名/作者 trim 后 1..200 字符 | FR-001、FR-011 |
| 身份和角色从哪里来? | 由受信任应用上下文提供在线账号;公开认证不在本期 | FR-010、FR-012 |
| 总库存能否低于借出数? | 不能;返回 TOTAL_BELOW_BORROWED,数据不变 | FR-002、FR-011 |
due_at == as_of 是否逾期? | 不逾期;只有 due_at < as_of 才为 OVERDUE | FR-008、FR-009 |
第二轮继续确认借期和并发:due_at = borrowed_at + 14 days;同一会员同一书目只允许一条 active loan;最后一本并发竞争恰好一个成功。会员只需存在,不检查欠费或逾期资格。
生成的文件及关键内容
【AI 产出】答案应直接写回 spec.md,而不是只留在聊天:
1### Clarifications
2- 2026-09-09: MVP 按书目聚合库存,不跟踪实体副本。
3- 2026-09-09: ISBN 去连字符后校验 ISBN-10/13;书名和作者长度为 1..200。
4- 2026-09-09: 身份由受信任应用上下文提供;本期不实现认证。
5- 2026-09-09: total_copies 不得小于 total_copies - available_copies。
6- 2026-09-09: 同一会员和书目最多一条 active loan。
7- 2026-09-09: 时间存库统一 UTC 口径,due_at = borrowed_at + 14 days。
8- 2026-09-09: returned_at 非空为 RETURNED;否则 due_at < as_of 为 OVERDUE,其余 ACTIVE。
9
10### Assumptions
11- 在线账号由应用上下文提供;角色只有 LIBRARIAN 和 MEMBER。
12- 借书只验证目标会员存在,不检查欠费或逾期资格。开发者需要人工确认
逐行检查 spec diff,确认答案没有被 AI 改写成不同语义;尤其核对 < 与 <=、UTC、重复 active loan 和库存粒度。
执行后的项目状态
spec.md 已包含足够的信息来决定数据模型和接口,不再有影响 plan 的隐藏假设。
常见错误、原因和修正
- 错误: 回答"按最佳实践"。原因:没有做业务决定。修正:明确
<、UTC、唯一性等语义。 - 错误: 答案只在聊天。原因:下一会话无法恢复。修正:确认已回写
spec.md。 - 错误: 用 Clarify 选择 ORM。原因:这是技术问题。修正:留给 Plan/Research。
本阶段完成检查清单
- 所有会改变数据模型的歧义已回答。
- 回答已写入
spec.md。 - FR-001、FR-002、FR-008、FR-010、FR-011、FR-012 已同步修订。
- 没有用技术方案掩盖业务决策。
4. Plan:把规格映射到技术方案
当前场景与目标
需求已稳定,现在需要决定技术栈、数据约束、事务策略、API、DDL、测试和目录。
开始前的项目状态
1spec.md # what/why 已澄清
2constitution.md # 项目 gate 已批准先写一页 Planning Brief
Plan 前把已经确定的技术边界和待调研项分开,避免它们散落在多轮提示词里。这页 planning-brief.md 是团队自定义输入,Spec Kit 不会自动生成或发现它:
1## 已确定
2- Java 8、Spring Boot 2、MyBatis-Plus 3.4、MySQL;沿用公司父 POM 管理依赖。
3- Maven 多模块:library-api(对外契约)、library-service(业务核心)、library-web(HTTP 入口)。
4- 身份由应用上下文提供在线账号,不实现认证。
5- 接口沿用本章固定契约(api/book、api/loan 动词式端点)。
6- 不引入缓存、消息队列、微服务拆分或额外的 repository 包装层(Mapper 已承担该职责)。
7
8## 待调研
9- MySQL 5.x 不执行列级 CHECK 约束,库存不变量由条件 UPDATE 与服务层校验共同保证。
10- MySQL 没有 partial unique index,"同一会员同一书目仅一条 active loan"需要等价实现。
11- ISBN-10/13 校验没有现成注解,决定自写最小校验还是引入工具库。
12
13## 交付和验证
14- 接口、DDL 脚本(向前/回滚)、自动化测试、quickstart、需求追溯表。
15- 当前开发完成不等于已部署。这一步适合技术边界已经讨论过、但 Plan 仍有研究项的功能。小而明确的 feature 不必额外维护该文件。
使用的命令与完整输入
1/speckit.plan
2使用 Java 8、Spring Boot 2、MyBatis-Plus 3.4、MySQL,Maven 多模块
3(library-api / library-service / library-web),统一工程风格:
4ResultDTO 统一响应、CodeEnum 七位错误码、Request/DTO/VO/Entity 四层模型、
5校验分组、knife4j 接口文档。research.md 必须记录 MySQL 5.x 的约束差异
6与并发条件更新的验证边界。
7
8接口契约(POST + 请求体,分页用 PageDTO,页码从 0 开始):
9- POST /api/book/create
10- POST /api/book/updateById
11- POST /api/book/queryPage
12- POST /api/loan/create
13- POST /api/loan/returnById
14- POST /api/loan/queryMemberPage
15- POST /api/loan/queryOverduePage
16
17身份由应用上下文提供在线账号(id、角色);LIBRARIAN 才能维护图书和办理借还,
18MEMBER 只能查询图书和本人借阅。ISBN、文本长度、库存和角色错误都要有稳定错误码
19(CodeEnum,业务错误 HTTP 状态码统一 200,靠 code 区分)。
20借书使用带 available_copies > 0 条件的原子 UPDATE,并在同一事务插入 Loan;
21"同一 member_id/book_id 仅一条 active loan"用冗余 active_flag 列 + 唯一索引
22(MySQL 无 partial unique index 的等价方案,归还时置 NULL)兜底并发。
23归还只允许从 active 状态原子迁移一次。所有时间按 UTC 口径,状态按 as_of 派生。
24
25测试使用 JUnit + spring-boot-starter-test(MockMvc);父 POM 默认 skipTests=true,
26测试命令必须显式 -DskipTests=false;并发测试必须真实并发,不能用顺序请求代替。
27输出 research.md、data-model.md、contracts/openapi.yaml、quickstart.md、
28向前/回滚 DDL 方案、真实项目目录与 Constitution Check。生成的文件
1specs/001-library-circulation/
2├── spec.md
3├── plan.md
4├── research.md
5├── data-model.md
6├── contracts/openapi.yaml
7├── quickstart.md
8└── checklists/requirements.mdplan.md 关键内容
【AI 产出】
1## Technical Context
2- Runtime: Java 8(maven.compiler.source/target 1.8)
3- Framework: Spring Boot 2(继承公司父 POM)
4- Persistence: MyBatis-Plus 3.4 + MySQL(Druid 连接池)
5- Test: JUnit + spring-boot-starter-test(MockMvc;父 POM 默认 skipTests,命令需 -DskipTests=false)
6- Docs: knife4j(Swagger 2 注解)
7
8## Constitution Check
9- Transactional Circulation: PASS,借还各使用单事务和条件更新。
10- Testable Business Rules: PASS,FR-001..FR-012 映射到 integration tests。
11- Stable External Errors: PASS,业务错误统一 ResultDTO + CodeEnum,HTTP 200。
12- Minimal Dependencies: PASS,未引入队列、缓存或额外 repository 抽象。
13
14## Project Structure
15library-system/ # Maven 父工程(packaging=pom)
16├── library-api/ # 对外契约:ResultDTO/PageDTO/CodeEnum
17├── library-service/
18│ └── src/main/java/com/example/library/
19│ ├── entity/ # BookEntity、LoanEntity(继承 BaseEntity)
20│ ├── mapper/ # BookMapper、LoanMapper(含 XML)
21│ ├── dto/ # BookDTO、LoanDTO、LoanCreateDTO 等
22│ ├── enums/ # RoleEnum、LoanStatusEnum(派生状态)
23│ └── service/ # BookService、LoanService + impl/
24├── library-web/
25│ └── src/main/java/com/example/library/
26│ ├── controller/ # BookController、LoanController
27│ ├── request/ # BookSaveRequest(校验分组)、LoanCreateRequest 等
28│ ├── request/validator/group/ # BookCreateValidationGroup 等
29│ ├── vo/ # BookVO、LoanVO
30│ └── advice/ # GlobalExceptionAdvice、GlobalResultAdvice
31├── library-web/src/main/resources/
32│ ├── application.yml + application-local.yml
33│ └── mapper/book/BookMapper.xml、mapper/loan/LoanMapper.xml
34├── docs/ddl/V001__library_schema.sql # 向前 DDL
35├── docs/ddl/U001__library_schema_rollback.sql
36└── library-web/src/test/java/com/example/library/
37 ├── BookCatalogTest.java
38 ├── BookSearchTest.java
39 ├── RoleBoundaryTest.java
40 ├── CirculationTest.java
41 └── OverdueTest.javadata-model.md 关键内容
1在线账号(User)
2- 不落库。由应用上下文提供 id(Long)与角色(RoleEnum: LIBRARIAN | MEMBER),
3 controller 层取值后显式传给 service。
4
5book
6- id: BIGINT, PK, AUTO_INCREMENT
7- isbn: VARCHAR(20), required, unique(uk_isbn)
8- title: VARCHAR(200), required
9- author: VARCHAR(200), required
10- total_copies: INT UNSIGNED, required
11- available_copies: INT UNSIGNED, required
12- create_time / update_time / is_delete(逻辑删除,沿用 BaseEntity)
13
14loan
15- id: BIGINT, PK, AUTO_INCREMENT
16- book_id: BIGINT, required
17- member_id: BIGINT, required(在线账号 ID)
18- member_name: VARCHAR(64)(回显用)
19- borrowed_at: DATETIME, required(UTC 口径)
20- due_at: DATETIME, required
21- returned_at: DATETIME, nullable
22- active_flag: TINYINT, nullable——在借时为 1,归还后置 NULL
23- create_time / update_time / is_delete
24
25Invariant: 同一 member_id + book_id 最多一条 active 记录。
26MySQL 无 partial unique index,等价实现为唯一索引
27uk_member_book_active(member_id, book_id, active_flag)——NULL 在唯一索引中可重复。
28Derived status(as_of): returned_at != NULL -> RETURNED; due_at < as_of -> OVERDUE; otherwise ACTIVE。【人工决策】MySQL 5.x 解析但忽略列级 CHECK 约束,0 <= available_copies <= total_copies 不能只写在 DDL 里。research.md 必须记录:不变量由条件 UPDATE(WHERE available_copies > 0)+ 服务层校验共同保证,INT UNSIGNED 再兜一层负值防线。
上面的模型落到 T004 要交付的 docs/ddl/V001__library_schema.sql(节选,字段与 data-model 一一对应):
1CREATE TABLE `book` (
2 `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
3 `isbn` VARCHAR(20) NOT NULL,
4 `title` VARCHAR(200) NOT NULL,
5 `author` VARCHAR(200) NOT NULL,
6 `total_copies` INT UNSIGNED NOT NULL,
7 `available_copies` INT UNSIGNED NOT NULL,
8 `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
9 `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
10 `is_delete` TINYINT NOT NULL DEFAULT 0,
11 PRIMARY KEY (`id`),
12 UNIQUE KEY `uk_isbn` (`isbn`)
13) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
14
15CREATE TABLE `loan` (
16 `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
17 `book_id` BIGINT UNSIGNED NOT NULL,
18 `member_id` BIGINT UNSIGNED NOT NULL,
19 `member_name` VARCHAR(64) DEFAULT NULL,
20 `borrowed_at` DATETIME NOT NULL,
21 `due_at` DATETIME NOT NULL,
22 `returned_at` DATETIME DEFAULT NULL,
23 `active_flag` TINYINT DEFAULT NULL COMMENT '在借=1,归还后置 NULL',
24 -- create_time / update_time / is_delete 同 book 表
25 PRIMARY KEY (`id`),
26 UNIQUE KEY `uk_member_book_active` (`member_id`, `book_id`, `active_flag`),
27 KEY `idx_member_borrowed` (`member_id`, `borrowed_at`)
28) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;回滚脚本 U001__library_schema_rollback.sql 做反向动作(DROP TABLE),quickstart 里会先升后降再升各跑一遍验证。旁边的 seed_demo.sql 插两条图书、一名演示账号和一笔记借阅——quickstart 的 10 分钟验证拿它当现成数据。
contracts/openapi.yaml 关键内容
响应统一包在 ResultDTO(code/message/success/model)里,业务错误 HTTP 状态码为 200:
1paths:
2 /api/book/create:
3 post:
4 responses:
5 "200":
6 description: 录入成功或返回参数/权限/重复错误码
7 /api/book/updateById:
8 post:
9 responses:
10 "200":
11 description: 编辑成功或 TOTAL_BELOW_BORROWED 等错误码
12 /api/book/queryPage:
13 post:
14 description: 关键词匹配书名/作者/ISBN,PageDTO 分页(页码从 0 开始)
15 /api/loan/create:
16 post:
17 responses:
18 "200":
19 description: 借阅成功或 BOOK_UNAVAILABLE / ACTIVE_LOAN_EXISTS
20 /api/loan/returnById:
21 post:
22 responses:
23 "200":
24 description: 归还成功或 LOAN_ALREADY_RETURNED
25 /api/loan/queryMemberPage:
26 post:
27 description: 会员本人借阅列表(含按 as_of 派生状态)
28 /api/loan/queryOverduePage:
29 post:
30 description: 指定时刻全部逾期借阅(仅 LIBRARIAN)错误码和角色边界
错误码遵循七位数字约定:模块标识(2 位)+ 错误来源(1 位,1 用户/2 系统/3 第三方)+ 错误标识(4 位)。假设图书模块编号为 30,spec 语义名与 CodeEnum 映射如下:
| spec 语义名 | CodeEnum | code | description |
|---|---|---|---|
| INVALID_ISBN | LIBRARY_USER_ERROR_0101 | 3010101 | ISBN 格式非法 |
| INVALID_BOOK_FIELD | LIBRARY_USER_ERROR_0100 | 3010100 | 图书参数非法 |
| FORBIDDEN | LIBRARY_USER_ERROR_0102 | 3010102 | 无权限执行该操作 |
| BOOK_NOT_FOUND | LIBRARY_USER_ERROR_0200 | 3010200 | 图书不存在 |
| LOAN_NOT_FOUND | LIBRARY_USER_ERROR_0201 | 3010201 | 借阅记录不存在 |
| ISBN_ALREADY_EXISTS | LIBRARY_USER_ERROR_0300 | 3010300 | ISBN 已存在 |
| TOTAL_BELOW_BORROWED | LIBRARY_USER_ERROR_0301 | 3010301 | 总库存不得小于当前借出数 |
| BOOK_UNAVAILABLE | LIBRARY_USER_ERROR_0400 | 3010400 | 图书无可借库存 |
| ACTIVE_LOAN_EXISTS | LIBRARY_USER_ERROR_0401 | 3010401 | 该会员已有同书在借记录 |
| LOAN_ALREADY_RETURNED | LIBRARY_USER_ERROR_0402 | 3010402 | 借阅记录已归还 |
【AI 产出】这些错误码是实现契约的示例,功能间步长预留 100。MEMBER 请求 /api/book/create、/api/book/updateById、/api/loan/create 或 /api/loan/returnById 都返回 FORBIDDEN;LIBRARIAN 才能执行管理操作。
有一处差异要明确写进契约,不能含糊:本案例的全局异常处理器约定业务错误 HTTP 状态码一律 200,客户端以 ResultDTO.code 判断结果。错误响应统一长这样:
1{
2 "code": 3010400,
3 "message": "图书无可借库存",
4 "success": false,
5 "model": null
6}message 可以本地化,code 作为稳定契约。内部 SQL、堆栈和数据库地址不进入响应。
research.md 决策格式
1## Decision: 条件 UPDATE 扣减库存
2Rationale: 单条 SQL(UPDATE book SET available_copies = available_copies - 1
3WHERE id = ? AND available_copies > 0)将可借条件与扣减绑定,行锁天然串行化
4最后一本竞争;返回影响行数 0 即视为 BOOK_UNAVAILABLE。
5Alternatives: 悲观锁 SELECT ... FOR UPDATE;应用层全局锁。
6MySQL 行为需在目标版本实测(本地与生产隔离级别差异写入测试边界)。
7
8## Decision: active_flag 冗余列 + 唯一索引阻止重复在借
9Rationale: MySQL 无 partial unique index;active_flag 在借为 1、归还置 NULL,
10uk_member_book_active 利用唯一索引忽略 NULL 的语义实现"仅一条 active",
11并发下靠 DuplicateKeyException 兜底并翻译为业务错误码。
12Alternatives: 归还后删除记录(丢失历史);应用层串行检查(无法兜住并发)。
13
14## Decision: 不引入任务队列和缓存
15Rationale: 当前需求是同步借还和查询;新增基础设施没有明确收益。
16Alternatives: Redis 队列;事件驱动库存。达到跨服务吞吐需求时再评估。quickstart.md 应包含
1mvn clean install -DskipTests=false # 父 POM 默认 skipTests=true,必须显式打开
2mysql -u root -e "CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4;"
3mysql -u root library < docs/ddl/V001__library_schema.sql
4mysql -u root library < docs/ddl/seed_demo.sql
5mvn --projects library-web spring-boot:run -Dspring-boot.run.profiles=local
6mvn --projects library-web test -DskipTests=false并给出录入、非法 ISBN、越权编辑、查询、借书、查看本人状态、还书和逾期查询的请求与期望的 ResultDTO.code。
quickstart.md 在 Plan 阶段只是"拟定验收方法"。此时没有代码可供验证。真正的运行证据要等 Implement 完成后再记录,不能因为 quickstart 写出来了就宣称验收通过。
开发者需要人工确认
- 技术栈是团队批准的,不是 AI 随机选择;
- 条件更新和唯一约束确实能在目标 MySQL 版本工作;
- 回滚 DDL 不会丢失已有数据;
- 契约文档中错误码、字段名与 FR 完全一致;
- Plan 没有加入 out-of-scope 的登录、通知或前端;
- HTTP 200 + code 的错误约定已同步给调用方确认。
执行后的项目状态
Spec 负责业务契约,Plan 与四类设计工件负责实现契约,尚未生成 tasks.md 或代码。
常见错误、原因和修正
- 错误: Plan 只输入 "Execute"。原因:agent 会自由选择架构。修正:给技术边界、复用点、DDL 和测试。
- 错误:
plan.md引用不存在目录。原因:没读仓库。修正:要求先探索并引用真实路径。 - 错误: 把本地 MySQL 测试当生产并发证明。修正:记录版本与隔离级别差异,在目标环境验证。
本阶段完成检查清单
- FR-001..FR-012 都能映射到数据、接口或测试设计。
- 接口路径与本章固定契约一致。
- 数据约束、事务、DDL 和回滚已人工评审。
-
research.md有 Decision/Rationale/Alternatives,而不是搜索结果堆积。
5. Checklist:先检查需求质量
当前场景与目标
拆任务前确认需求是否完整、清晰、一致。Checklist 检查的是需求文本,不是代码完成度。
使用的命令与完整输入
1/speckit.checklist
2重点检查 FR-001..FR-012 的字段校验、角色边界、事务、库存不变量、重复操作、并发竞争、
3UTC 时间语义、稳定错误码,以及 SC-001..SC-003 是否可验证。生成内容
【AI 产出,评审者所有】
1- [ ] 是否定义了同一会员重复 active loan 的结果?
2- [ ] 是否定义了最后一本并发竞争的唯一结果?
3- [ ] 是否明确 due_at == as_of 不算逾期?
4- [ ] 是否说明重复归还不得增加库存?
5- [ ] 是否为每种冲突定义稳定错误码?
6- [ ] 是否说明 SC-003 的计时起点和环境?【人工决策】逐条审查,发现缺口就回 /speckit.clarify 或 /speckit.specify。只有评审者确认后才勾选。/speckit.implement 不应替评审者修改这些 checkbox。
执行后的项目状态
需求质量 gate 已通过,可以从已批准工件生成任务。
本阶段完成检查清单
- 每个
[x]都有人工判断依据。 - 缺口已修回
spec.md,不是在 checklist 里补一句就算完成。 - Checklist checkbox 没有被当成实施进度。
6. Tasks:生成可执行、可追溯的工作包
当前场景与目标
需要把工件拆成有依赖、含文件路径、按用户故事组织的任务。字段约束与错误码不能在这一层丢失。
使用的命令
1/speckit.tasks生成的 tasks.md
【AI 产出,人工修订后作为实施基线】
1## Phase 1: Setup
2- [ ] T001 创建父 pom 与 library-api/library-service/library-web 模块骨架,继承公司父 POM,锁定 Spring Boot 2、MyBatis-Plus 3.4
3- [ ] T002 [P] 建立 service 模块包结构(entity/mapper/dto/enums/service)与 web 模块包结构(controller/request/vo/advice)及测试目录
4
5## Phase 2: Foundational
6- [ ] T003 定义 BookEntity/LoanEntity(继承 BaseEntity,含 active_flag)、RoleEnum/LoanStatusEnum 及全部字段约束 library-service/src/main/java/com/example/library/entity
7- [ ] T004 创建向前 DDL docs/ddl/V001__library_schema.sql 与回滚 docs/ddl/U001__library_schema_rollback.sql(含 uk_member_book_active)
8- [ ] T005 [P] 新增 CodeEnum 图书模块错误码(七位编码)与 GlobalExceptionAdvice/GlobalResultAdvice library-api、library-web/.../advice
9
10## Phase 3: US1 Maintain catalog (P1)
11- [ ] T006 [P] [US1] 编写 FR-001/FR-002/FR-010/FR-011 录入、编辑、非法 ISBN、库存下限和越权测试 library-web/src/test/java/com/example/library/BookCatalogTest.java
12- [ ] T007 [US1] 实现 BookSaveRequest(校验分组)、BookController 的 /api/book/create 与 /api/book/updateById 及 BookService 实现 library-web/.../controller、library-service/.../service/impl
13
14## Phase 4: US2 Search books (P1)
15- [ ] T008 [P] [US2] 编写 FR-003 搜索、空结果、角色可见性测试 library-web/src/test/java/com/example/library/BookSearchTest.java
16- [ ] T009 [US2] 实现 BookQueryRequest 与 /api/book/queryPage(PageDTO 分页,页码从 0 开始) library-web/.../request、library-web/.../controller
17
18## Phase 5: US3 Borrow and return (P1)
19- [ ] T010 [P] [US3] 编写 FR-004/FR-005/FR-006 借书测试 library-web/src/test/java/com/example/library/CirculationTest.java
20- [ ] T011 [US3] 编写 SC-002 真实并发测试(两个线程经 CountDownLatch 同时调用借书) library-web/src/test/java/com/example/library/CirculationTest.java
21- [ ] T012 [US3] 实现条件库存扣减 UPDATE 与借阅事务 BookMapper.xml、library-service/.../service/impl/LoanServiceImpl.java
22- [ ] T013 [US3] 实现 LoanCreateRequest 与 /api/loan/create library-web/.../controller
23- [ ] T014 [US3] 编写 FR-007 重复归还测试 library-web/src/test/java/com/example/library/CirculationTest.java
24- [ ] T015 [US3] 实现归还服务(条件迁移 returned_at/active_flag)与 /api/loan/returnById,修改 library-service/.../service/impl/LoanServiceImpl.java 和 library-web/.../controller/LoanController.java
25
26## Phase 6: US4 View loan status (P1)
27- [ ] T016 [P] [US4] 编写 FR-008/FR-009 状态边界和 MEMBER 隔离测试 library-web/src/test/java/com/example/library/RoleBoundaryTest.java
28- [ ] T017 [US4] 实现 /api/loan/queryMemberPage(as_of 派生状态) library-web/.../controller、library-service/.../service/impl
29
30## Phase 7: US5 Overdue loans (P2)
31- [ ] T018 [P] [US5] 编写 FR-008/FR-009 due_at < as_of 和角色测试 library-web/src/test/java/com/example/library/OverdueTest.java
32- [ ] T019 [US5] 实现 /api/loan/queryOverduePage library-web/.../controller、LoanMapper.xml
33
34## Final Phase: Polish
35- [ ] T020 运行完整测试(-DskipTests=false)、DDL 升级/回滚及契约检查(knife4j 导出与 contracts/openapi.yaml 比对)
36- [ ] T021 在干净环境执行 quickstart,记录 SC-003 结果任务追溯矩阵
| 需求/标准 | 接口或规则 | 任务 |
|---|---|---|
| FR-001、FR-002、FR-010、FR-011 | /api/book/create、/api/book/updateById | T003-T007 |
| FR-003 | /api/book/queryPage | T008-T009 |
| FR-004、FR-005、FR-006、FR-011 | /api/loan/create、事务、错误码 | T003、T005、T010-T013 |
| FR-007、FR-011 | /api/loan/returnById | T005、T014-T015 |
| FR-008、FR-009 | /api/loan/queryMemberPage、/api/loan/queryOverduePage | T016-T019 |
| SC-001 | 全部 P1 验收测试 | T006、T008、T010、T014、T016、T020 |
| SC-002 | 真实并发 | T011、T012、T020 |
| SC-003 | 干净环境 quickstart | T020-T021 |
开发者需要人工确认
- T003 是否把
data-model.md的约束原样带入(含 active_flag 与唯一索引); - T009 与 T008 修改同一模块,因此 T009 没有
[P]; - 每个用户故事是否有独立测试和 checkpoint;
- 没有任务实现 out-of-scope。
执行后的项目状态
tasks.md 成为实施顺序,但还没有代码。下一步先做只读一致性分析。
常见错误、原因和修正
- 错误:"实现借还功能"一个任务。原因:无法审查或恢复。修正:按测试、约束、服务、端点拆分。
- 错误: 任务没有文件路径。原因:agent 需要重新猜影响面。修正:指向真实文件。
- 错误:
data-model.md写长度/nullable,tasks 丢失。修正:在对应实体或 DDL 任务原样列出。
本阶段完成检查清单
- T001..T021 连续且没有重复 ID。
- 追溯矩阵覆盖 FR-001..FR-012、SC-001..SC-003。
-
[P]不会造成同文件或依赖冲突。 - 每个任务有明确文件或验证命令。
7. Analyze:编码前检查跨文档一致性
当前场景与目标
Spec、Plan、Tasks 各自看起来合理,但可能存在需求无任务、任务无需求、字段不一致或 Constitution 冲突。
使用的命令
1/speckit.analyze【官方能力】【版本相关】v1.0.5 支持该只读命令。它报告问题,不修改文件。
示例报告与修复
1HIGH: FR-006 要求数据库层阻止重复 active loan,T004 只写"唯一索引",
2 未说明 active_flag 置 NULL 的归还路径与该索引的配合方式。
3MEDIUM: T010 与 T011 都修改 CirculationTest.java,不能同时并行。
4MEDIUM: SC-003 要求 10 分钟内完成,但 quickstart 未定义计时起点。修复归属:
- FR-006 的方案缺口回
research.md、data-model.md、plan.md,再更新 T004/T015; - 并行标记错误回
tasks.md,确保 T010/T011 串行; - 测量定义回
spec.md,再更新quickstart.md与 T021; - 重跑
/speckit.analyze。
人工确认
Analyze clean 仅代表 agent 未发现跨工件问题,不代表架构正确、安全审查完成或测试通过。
执行后的项目状态
所有阻断项已修到源工件,tasks.md 可作为实施输入。
本阶段完成检查清单
- Analyze 在实施前运行。
- 每条问题按需求/设计/任务归属回源修正。
- 修正后重新运行,阻断项清零。
- 没有用改代码掩盖文档冲突。
8. Implement:按阶段编码,而不是一次吞完
当前场景与目标
T001..T021 已批准。为控制上下文和回归,先完成 Setup/Foundational,再逐个用户故事实施。
第一轮:T001-T005
1/speckit.implement
2只执行 Phase 1 和 Phase 2(T001-T005)。运行 DDL 脚本、实体/错误码相关测试;
3报告修改文件、命令、退出码和未完成项。遇到未决业务规则时停止,
4不要实现接口或加入 out-of-scope 功能。结束状态:模块骨架、实体、DDL 和错误映射存在;尚未实现用户故事。
第二轮:T006-T015
1/speckit.implement
2执行 Phase 3 至 Phase 5(T006-T015)。先运行对应测试观察失败,再实现;
3每完成 US1、US2 或 US3 就运行该故事的测试(记得 -DskipTests=false)。
4T011 必须使用两个真实并发线程(ExecutorService + CountDownLatch 同时进入
5service 事务),不能用两个顺序请求冒充。不要实现 US4/US5。【AI 产出】借书核心代码可能类似:
1@Service
2@Slf4j
3public class LoanServiceImpl implements LoanService {
4
5 @Resource
6 private BookMapper bookMapper;
7
8 @Resource
9 private LoanMapper loanMapper;
10
11 @Override
12 @Transactional(rollbackFor = Exception.class)
13 public Long createLoan(LoanCreateDTO createDTO) {
14 // 应用层先挡一道明显的重复在借;并发漏网的交给唯一索引兜底
15 Long activeCount = loanMapper.selectCount(
16 new LambdaQueryWrapper<LoanEntity>()
17 .eq(LoanEntity::getMemberId, createDTO.getMemberId())
18 .eq(LoanEntity::getBookId, createDTO.getBookId())
19 .isNull(LoanEntity::getReturnedAt));
20 if (activeCount > 0) {
21 throw new FailedException(CodeEnum.LIBRARY_USER_ERROR_0401.getCode());
22 }
23
24 // 条件更新把“可借”与“扣减”绑在同一条 SQL 上,最后一本竞争由行锁串行化
25 int deducted = bookMapper.deductAvailableCopy(createDTO.getBookId());
26 if (deducted != 1) {
27 throw new FailedException(CodeEnum.LIBRARY_USER_ERROR_0400.getCode());
28 }
29
30 Date borrowedAt = new Date();
31 LoanEntity loan = LoanEntity.builder()
32 .bookId(createDTO.getBookId())
33 .memberId(createDTO.getMemberId())
34 .memberName(createDTO.getMemberName())
35 .borrowedAt(borrowedAt)
36 .dueAt(DateUtil.offsetDay(borrowedAt, LoanConstants.LOAN_PERIOD_DAYS))
37 .activeFlag(LoanConstants.ACTIVE_FLAG_ON)
38 .build();
39 try {
40 loanMapper.insert(loan);
41 } catch (DuplicateKeyException ex) {
42 // uk_member_book_active 冲突说明并发下已产生在借记录,翻译为业务错误码
43 log.warn("[LoanServiceImpl.createLoan] 并发重复借阅冲突(uk_member_book_active),"
44 + "会员ID: [{}],图书ID: [{}]",
45 createDTO.getMemberId(), createDTO.getBookId());
46 throw new FailedException(CodeEnum.LIBRARY_USER_ERROR_0401.getCode());
47 }
48 return loan.getId();
49 }
50}条件扣减写在 BookMapper.xml(更新 book 表的原子写操作落到 XML,单表链式查询仍可用 MyBatis-Plus):
1<update id="deductAvailableCopy">
2 UPDATE `library`.`book`
3 SET `available_copies` = `available_copies` - 1
4 WHERE `id` = #{bookId}
5 AND `available_copies` > 0
6 AND `is_delete` = 0
7</update>录入图书的校验分两层:字段格式进 BookSaveRequest 注解,ISBN 业务规则在 service:
1@Data
2@Builder
3@NoArgsConstructor
4@AllArgsConstructor
5@ApiModel(value = "图书新增/编辑请求", description = "图书新增/编辑请求")
6public class BookSaveRequest implements Serializable {
7
8 private static final long serialVersionUID = 1L;
9
10 @Null(message = "新增时不能传图书 ID", groups = BookCreateValidationGroup.class)
11 @NotNull(message = "图书 ID 不能为空", groups = BookUpdateValidationGroup.class)
12 @Positive(message = "图书 ID 必须为正数", groups = BookUpdateValidationGroup.class)
13 @ApiModelProperty(value = "图书 ID(编辑时必传,新增禁止传)")
14 private Long id;
15
16 @NotBlank(message = "ISBN 不能为空")
17 @ApiModelProperty(value = "ISBN(ISBN-10 或 ISBN-13,允许连字符)", required = true)
18 private String isbn;
19
20 @NotBlank(message = "书名不能为空")
21 @Size(max = 200, message = "书名长度不能超过 200")
22 @ApiModelProperty(value = "书名", required = true)
23 private String title;
24
25 @NotNull(message = "总库存不能为空")
26 @PositiveOrZero(message = "总库存必须为非负整数")
27 @ApiModelProperty(value = "总库存(非负整数)", required = true)
28 private Integer totalCopies;
29
30 public BookSaveDTO toSaveDTO() {
31 return BookSaveDTO.builder()
32 .id(this.id)
33 .isbn(this.isbn)
34 .title(this.title)
35 .totalCopies(this.totalCopies)
36 .build();
37 }
38}1private void validateIsbnOrThrow(String isbn) {
2 String normalized = StringUtils.deleteWhitespace(isbn.replace("-", ""));
3 if (!IsbnValidator.isIsbn10Or13(normalized)) {
4 throw new FailedException(CodeEnum.LIBRARY_USER_ERROR_0101.getCode());
5 }
6}【人工决策】IsbnValidator.isIsbn10Or13 是自写最小校验还是引入工具库、是否允许 ISBN-13 校验位错误、空白如何处理,必须在 Plan/Research 中定下来。不要把示例函数当成官方实现。
这段代码不能单独证明正确。人工审查至少要问:
- 重复在借检查、库存扣减和插入是否处于同一事务(
rollbackFor = Exception.class是否覆盖非受检异常之外的路径); - 唯一索引是否兜住并发重复借阅,
DuplicateKeyException的约束名判断是否必要; FailedException抛出后事务是否正确回滚(全局异常处理器只做翻译,不动事务);- member 不存在时返回什么;
- 目标 MySQL 的隔离级别是否满足条件更新的假设。
第三轮:T016-T021
1/speckit.implement
2执行 Phase 6、Phase 7 和 Final Phase(T016-T021)。实现借阅状态与逾期查询;
3运行完整测试(-DskipTests=false)、DDL 升级/回滚、契约检查(knife4j 导出比对
4contracts/openapi.yaml);在干净环境按 quickstart 计时。不要把测试通过描述为已部署。自定义实施报告
实现结束后保留一份逐项实施报告,便于验收时逐条对证据。它不是 Spec Kit v1.0.5 的标准核心产物;如果团队不需要额外报告,可以直接使用 tasks.md、测试结果和 PR 描述。对本案例,建议创建:
1specs/001-library-circulation/implementation-report.md最小模板:
1## Scope
2实现 T001-T021,未实现 Out of Scope。
3
4## Traceability
5| Requirement | Implementation | Test / evidence | Result |
6| FR-001 | library-web/.../controller/BookController.java | BookCatalogTest.java | passed |
7| FR-005 | LoanServiceImpl + LoanMapper.xml + V001 DDL | CirculationTest.java | passed |
8
9## Commands
10- `mvn --projects library-web test -DskipTests=false`: [真实结果]
11- `mysql -u root library < docs/ddl/V001__library_schema.sql`: [真实结果]
12- `mysql -u root library < docs/ddl/U001__library_schema_rollback.sql`: [真实结果]
13
14## Deviations and open items
15[没有运行、失败、阻塞、人工验收和 Plan 偏离必须逐项写明]passed、failed、not run、blocked 不能混为“完成”。
执行后的项目状态
1library-system/
2├── .specify/
3├── specs/001-library-circulation/
4│ ├── spec.md
5│ ├── plan.md
6│ ├── research.md
7│ ├── data-model.md
8│ ├── contracts/openapi.yaml
9│ ├── quickstart.md
10│ ├── checklists/requirements.md
11│ └── tasks.md # T001..T021 已更新状态
12├── docs/ddl/V001__library_schema.sql
13├── library-api/
14├── library-service/
15└── library-web/ # 含 src/test/java/com/example/library/实施中可以改代码;固定候选版本做验收时,发现缺陷应先记录并停止本轮,修复后重新生成候选版本、重新跑回归,再更新实施报告。正式运行中不应由处理用户请求的 Agent 顺手修改生产代码。
常见错误、原因和修正
- 错误: 一次实施全部任务。原因:上下文、风险和 diff 过大。修正:按 foundation/故事分批。
- 错误: agent 说“完成”即接受。原因:没有运行证据。修正:要求命令、退出码、未执行项。
- 错误: 为“未来扩展”引入 repository/factory/cache。原因:YAGNI,Mapper 已是数据访问层。修正:先实现已批准 plan。
- 错误: 跑
mvn test显示全绿就当测试通过。原因:父 POM 默认skipTests=true,实际什么都没跑。修正:命令必须带-DskipTests=false并记录退出码。
本阶段完成检查清单
- 每一轮只修改约定范围。
- 每个用户故事结束时都有测试结果与 diff review。
- SC-002 使用真实并发测试。
- T020/T021 的命令与结果已记录。
9. 测试验收:区分四种完成
自动化证据
记录真实命令和输出,不写模糊的“测试已通过”:
1mvn --projects library-web test -DskipTests=false
2Tests run: 28, Failures: 0, Errors: 0, Skipped: 0
3exit 0
4
5mysql -u root library < docs/ddl/V001__library_schema.sql
6exit 0
7
8mysql -u root library < docs/ddl/U001__library_schema_rollback.sql
9exit 0
10
11mysql -u root library < docs/ddl/V001__library_schema.sql
12exit 0验收矩阵
| 验收项 | 关联 | 自动化 | 人工确认 |
|---|---|---|---|
| 图书维护与字段校验 | FR-001、FR-002、FR-011、T003-T007 | BookCatalogTest 录入/编辑/非法 ISBN/库存下限 | 错误码和角色边界稳定 |
| 部分书名/ISBN 搜索及库存 | FR-003、T008-T009 | BookSearchTest(MockMvc) | 返回字段满足管理员需求 |
| 正常借书与 14 天 due_at | FR-004、T010-T013 | 固定时钟测试 | 时间语义正确 |
| 最后一本并发竞争 | FR-005、SC-002、T011-T012 | 两线程真实并发测试 | 目标数据库已验证 |
| 重复 active loan | FR-006、T003、T010 | 唯一索引与接口测试 | 冲突码稳定 |
| 重复归还 | FR-007、T014-T015 | 库存不变测试 | 操作反馈可理解 |
| 图书维护与角色 | FR-001、FR-002、FR-010、FR-011、T003-T007 | BookCatalogTest 越权场景 | 字段和库存规则正确 |
| 逾期边界 | FR-008、FR-009、T016-T019 | < as_of 边界测试 | 产品同意边界 |
| 错误响应 | FR-011、T005、T006、T010、T014、T018 | 契约检查(ResultDTO 结构) | 不泄露内部异常 |
| 干净环境启动 | SC-003、T020-T021 | quickstart 实跑 | 计时与环境已记录 |
并发测试的等价实现说明。MockMvc 跑在单线程 mock 环境里,证明不了 servlet 层的并发。SC-002 的做法是让两个线程经 CountDownLatch 同时进入 LoanServiceImpl.createLoan(事务边界在 service 层),断言恰好一个成功、available_copies 不为负。它和对容器发起两个并发 HTTP 请求验证的是同一份数据库行为,只是入口换了一层。
四种状态
- 已实施:代码存在;
- 已测试:指定测试实际通过;
- 已验收:产品/工程/测试确认需求;
- 已部署/已上线验证:发布物进入目标环境并验证。
不能用前一种状态替代后一种。
版本基线
把以下内容绑定到一个 commit 或 tag:
1spec.md + plan.md + tasks.md
2 ↓
3代码 + 测试
4 ↓
5implementation-report.md
6 ↓
7V1 candidate如果验收时修改了代码,原来的 V1 测试结论只对修改前版本有效。新问题应先分类:实现缺陷回代码/测试,需求遗漏回 spec/clarify,技术方案问题回 research/plan,新功能建立新 feature。这是工程纪律,不是 Spec Kit 自动执行的流程。
10. Converge:查漏,不替代验收
使用的命令
1/speckit.converge【官方能力】只有 implement 已针对当前 tasks.md 运行后才执行。它可能报告 Converged,也可能只向 tasks.md 追加任务。
示例:
1Tasks appended:
2- T022 [US3] 验证重复归还后 available_copies 未变化
3- T023 补 quickstart 中错误码响应示例继续:
1/speckit.implement
2只执行 Convergence 新增的 T022-T023,运行相关测试并报告结果。
3
4/speckit.converge直到:
1Converged — implementation satisfies the spec, plan, and tasks.【人工决策】即使 Converged,也仍需常规 code review、产品验收、CI 和部署验证。
11. 规格与实现回溯
实施过程中常发现新事实。不要只改代码,先判断哪个工件拥有这个事实。
| 发现 | 首先修改 | 后续动作 |
|---|---|---|
| 借期从 14 天改为按自然日截止 | spec.md | 重跑/修订 plan、tasks、analyze |
| 单库 MySQL 无法满足目标并发/容量 | research.md、plan.md | 评估 Sharding-JDBC 分库分表,更新 DDL 和 T004/T012/T016 |
| 契约文档缺少 ResultDTO 错误结构 | contracts/openapi.yaml、tasks.md | 增加 contract task/test |
| 实现发现事务边界与异常翻译需统一处理 | plan.md | 人工批准后更新 tasks 与 advice 代码 |
| 仅漏一个测试 | tasks.md | 追加任务并 implement/converge |
最终交付记录应明确:
1Artifact review: spec/plan/tasks approved
2Automated tests: Tests run: 28, Failures: 0, Errors: 0
3DDL upgrade/rollback: passed
4Quickstart: passed in 7m42s from clean checkout
5Product acceptance: US1/US2/US3/US4 accepted; US5 accepted separately if included
6Spec Kit converge: Converged
7Deployment: not performed12. 交付收尾:把借还能力登记进 OpenSpec
V1 验收通过后,第 2 章初始化的 openspec/ 还是空壳。交付的收尾动作是把已验收的行为登记成现行能力规格:内容来源就是本章的 spec.md(FR-001..FR-012 与验收场景),但登记的角度不同——写的是"系统现在怎么表现",供后续变更对照,不是当时的 feature 快照。
【AI 产出】openspec/specs/circulation/spec.md(节选;完整版由 FR 逐条翻译,人工核对每条 Scenario 与验收矩阵一致):
1# Circulation Specification
2
3## Purpose
4图书目录维护与借还流转能力:LIBRARIAN 维护书目并办理借还,
5MEMBER 查询书目与本人借阅。
6
7## Requirements
8
9### Requirement: Catalog maintenance
10系统 SHALL 允许 LIBRARIAN 录入与编辑图书(ISBN 唯一、库存非负整数);
11MEMBER 不得执行维护操作。
12
13#### Scenario: 重复 ISBN 被拒绝
14- **GIVEN** 某 ISBN 已存在
15- **WHEN** 再次录入同一 ISBN
16- **THEN** 返回 ISBN_ALREADY_EXISTS 错误码,库存不变
17
18### Requirement: Atomic borrow and return
19借书与库存扣减 SHALL 在同一事务内完成;并发竞争最后一本时恰好一个
20请求成功;归还 SHALL 只成功一次并原子恢复库存。
21
22### Requirement: Derived loan status
23借阅状态 SHALL 按查询时刻派生:已归还为 RETURNED,未归还且逾期为
24OVERDUE,其余为 ACTIVE。写法沿用第 5 章会展开的约定:Requirement 正文用 SHALL 表达义务,Scenario 用 GIVEN/WHEN/THEN;只写行为契约,事务管理器、条件 UPDATE 这些实现细节不进 spec。校验并提交:
1openspec validate --specs
2git add openspec && git commit -m "20260910_library-system_seedCirculationSpec"登记一次即可。此后"借期从 14 天改为自然日截止""逾期自动提醒"这类维护期增量,不再开完整 feature 目录,而是走第 5 章演示的变更提案循环:MODIFIED 那条 Requirement、评审差异、落地归档,现行规格始终保持与代码同步。
主案例最终检查清单
- Constitution、Spec、Plan、Tasks、代码的规则没有互相矛盾。
- FR-001..FR-012、SC-001..SC-003 均可追溯到任务和证据。
- 接口路径从 Plan 到测试保持一致。
- T001..T021 的依赖和文件路径成立。
- Analyze 阻断项已回源修正。
- Implement 分阶段完成,每阶段有测试和 review。
- Converge 最终报告 Converged,且未被误写成部署完成。
- 所有实施发现已反馈到相应工件。
- 已交付行为已登记进
openspec/specs/circulation/spec.md并通过openspec validate --specs。
主案例到此走完了从 0 到 1 的全流程。上线之后,图书系统同样会进入维护期——借期规则微调、逾期提醒这类小增量会不断到来。到那时不必每次都开完整 feature 目录:第 5 章会在老项目上用 OpenSpec 的"变更提案 + 归档合并回现行规格"轻循环管理这类日常增量。下一个实战(第 4 章)先演示在已有仓库里再做一次 Spec Kit 增量。