Checkout 优惠码:从松散需求到规格包

这是首页示例的完整版本:一个模糊需求被整理成范围清楚的 spec、实现切片、验收标准和可检查证据。

风险金额、总价、无效状态
边界付款前应用一个优惠码
证据校验、总价、回滚检查

写 spec 之前的需求

弱工单

给 checkout 加优惠码。
无效码不要影响付款。

Spec-First 改写

Feature: Checkout coupon code
Owner: Web Checkout
Status: Ready for review

Context:
- Checkout UI 位于 apps/web/routes/checkout。
- Totals 由 packages/billing/totals.ts 计算。
- Coupon 表已存在,但还没有接入 checkout。

Goal:
- 允许登录用户在付款前应用一个有效优惠码。
- 对无效、过期、已使用的 code 显示内联错误。

Non-goals:
- 不做优惠码后台管理。
- 不支持优惠叠加。
- 不重写 billing totals。

评审真正需要看的规格包

tasks.md

- [ ] 按 code 和用户查询 coupon。
- [ ] 校验过期、禁用和已使用状态。
- [ ] 用一个折扣重新计算 checkout total。
- [ ] 不清空付款表单地显示内联错误。
- [ ] 加 feature flag 作为回滚开关。

acceptance-criteria.md

- Given 一个有效优惠码
  When 用户在付款前应用它
  Then 页面展示折扣后总价,并用它发起付款。

- Given 一个过期码
  When 用户应用它
  Then 页面显示内联错误,原始总价保持不变。

test-evidence.md

自动化:
- 优惠码校验单元测试
- checkout total 集成测试
- 已使用优惠码回归测试

手工:
- 有效码截图
- 无效码截图
- 回滚开关说明

AI coding guardrail

只实现 checkout 优惠码应用。
不要新增后台管理、叠加规则、价格规则或 totals 重构。
每个改动文件都必须对应一个任务和一条验收标准。

为什么它能提前减少返工

保护计费边界

spec 指明 totals 模块,也明确禁止无关重构,避免 AI 编码把任务做大。

让错误可测试

过期、已使用、无效优惠码都变成具体验收路径,而不是 QA 后期才发现的问题。

给发布停止信号

回滚开关和证据清单让评审者知道指标异常时如何暂停变更。

评审时应该看什么

第一轮评审不要先问“模型能不能做优惠码”,而是问“它能不能在不动无关金额逻辑的情况下改 checkout”。所以规格包会点名 totals 模块、禁止优惠叠加,并把后台管理排除在外。这些决定让任务变小,也让评审更明确。

第二轮评审要看无效状态。优惠码功能最常见的问题并不花哨:过期码仍然影响总价、已使用优惠码第二次通过、错误后付款表单被清空、回滚时 UI 消失但旧计算路径没有恢复。薄需求不会暴露这些情况;可用的规格包会在实现前把它们写成验收标准。

范围检查

diff 应该只触及 checkout 优惠码查询、校验、总价展示和回滚开关。后台规则、叠加、价格计划和 billing 重构都需要单独规格。

证据检查

评审者需要看到有效码、无效码、过期码、已使用码路径,以及证明付款金额和展示金额一致的 total 计算测试。

发布检查

规格包里要写明功能开关,并说明第一轮发布时哪个指标或投诉模式会触发回滚。

如何把这个案例迁移到你的项目

当改动触及用户能看到或可能争议的数值时,可以套用这个案例:金额、积分、额度、权益、席位、配额或用量。具体优惠规则不一定适合你的产品,但评审结构通常适用。先命名计算的事实来源,再列出不能破坏它的状态:无效输入、过期权益、重复请求、部分灰度和回滚。

关键习惯是在实现前写出失败模式。如果团队等到 QA 才发现重复折扣或过期总价,修复通常会比原功能触及更多代码。规格包把这些问题前移到计划阶段,这时改方案成本最低。

如果使用 AI 编码助手,把非目标直接放在实现提示词上方,并要求它说明每个文件改动对应哪一个任务。如果它提出 totals 重构、后台管理、优惠叠加或价格模型调整,这应该变成新的规格,而不是塞进当前 diff。

评审时应拒绝的反模式

即使 demo 看起来正确,也要在评审中拒绝这些模式。Checkout 问题经常藏在状态转换、回滚行为,以及页面展示金额和支付系统实际扣款不一致的地方。

只在 UI 里算折扣

展示折扣和实际扣款必须来自同一个事实来源,否则客服会看到不一致总价。

意外支持优惠叠加

如果规格没写叠加,实现就应该拒绝或替换第二个 code,而不是发明规则。

只有开关没有数据检查

功能开关不等于安全回滚;已应用优惠不能让订单、总价或分析数据处于不一致状态。

复用这个案例前的最后检查

把这个案例用于自己的 checkout 前,先确认优惠逻辑的事实来源在哪里:后端服务、billing 包、数据库规则,还是支付渠道返回值。规格里必须明确页面展示金额和实际扣款金额如何保持一致,否则 UI 看起来正确也不能证明功能安全。

还要补上失败后的用户体验。无效码、过期码、已使用码、网络重试和灰度回滚,都应该保留原始总价并避免清空付款状态。这个检查能防止页面只在 happy path 演示中成立,实际发布后却在边界状态里制造客服工单。

这也是 checkout 类规格必须写证据的原因。

QA 和客服评审记录

优惠码规格不能只给工程看。真正出问题时,QA 会先看到复现路径,客服会先听到用户投诉,所以规格里要提前写清他们要验证什么、如何判断严重程度。

Review note

QA fixture:
- valid_20_percent_coupon
- expired_coupon
- reused_coupon_for_same_user
- disabled_coupon
- second_coupon_attempt

客服处理:
- 无效码:说明原始总价未改变。
- 过期码:记录活动来源和用户输入的 code。
- 总价不一致:立即升级给 billing owner。

回滚触发:
- 页面展示金额、订单金额、支付渠道扣款任一不一致。
- 首轮发布窗口内出现 5 个以上总价不一致工单。

这段记录让案例更接近真实上线流程。优惠码不是一个孤立的表单校验,而是会进入支付、客服和账务对账的金额路径。

上线后还要观察什么

Checkout 变更需要短观察窗口,因为真实优惠码、支付重试和客服流程接触功能后,才会暴露演示里看不到的问题。

总价不一致

观察页面展示金额、订单金额和支付渠道扣款是否一致。任何不一致都应该暂停 rollout。

无效码噪音

把无效、过期、已使用错误和支付失败分开记录,避免把 UX 困惑误判为计费故障。

观察结束后,再决定是否扩展到叠加、后台管理或更复杂价格规则;这些都应该进入新的规格,而不是追加到当前 diff。

把这个模式用于下一次 checkout 改动

先用生成器起草规格包,再把它贴进 issue 或 PR,最后再让 AI 编码助手实现。

编辑说明

这个案例故意保持小而具体:重点是展示怎样用规格边界阻止 AI 生成实现偏离需求。