通知偏好:从设置需求到迁移规格

这个案例展示如何把“让用户管理通知”这种宽泛需求,改写成包含同意规则、老用户迁移、重复发送证据和 AI 编码护栏的规格包。

风险同意状态、重复发送、用户信任
边界只处理邮件和推送
证据迁移、退订、投递检查

原始弱需求

原始工单

添加通知偏好设置。

用户应该能开启或关闭邮件和推送。
注意不要骚扰用户。

Spec-First 改写

Feature: Notification preference migration
Owner: Lifecycle Messaging
Status: Ready for review

Context:
- 现有用户的 email_opt_out 存在 users 表。
- Push token 存在 device_tokens。
- 营销邮件已经遵守 unsubscribe 记录。

Goal:
- 为产品邮件和推送建立统一偏好模型。
- 迁移时保留所有现有退订状态。
- 旧 reader 和新 reader 共存期间避免重复发送。

Non-goals:
- 不做 SMS 偏好。
- 不重设计营销邮件。
- 不做后台批量编辑。
- 不新增通知历史页面。

规格必须迫使团队决定什么

默认状态

已有退订保持关闭;没有显式退订的用户继续保持原投递行为,直到主动修改设置。

双读窗口

旧 reader 和新 reader 可以共存一个发布周期,但每类通知只能选择一个事实来源来决定是否投递。

渠道归属

本次只负责产品邮件和推送。营销退订规则和 SMS 同意状态不进入当前 diff。

评审真正需要看的规格包

spec.md

Preference categories:
- product_updates_email
- product_updates_push
- billing_alert_email
- billing_alert_push

Rules:
- opt-out 记录迁移为 disabled preference。
- billing alert 本次不能关闭。
- push disabled 表示不再入队新的 push job。
- email disabled 表示 delivery worker 抑制发送。

tasks.md

- [ ] 新增 preference 表和回填脚本。
- [ ] 新增 settings UI 的读取 API。
- [ ] 新增带 audit 字段的更新 API。
- [ ] 更新 email worker 抑制逻辑。
- [ ] 更新 push enqueue 抑制逻辑。
- [ ] 添加 dual-read 安全测试。

acceptance-criteria.md

- Given 已存在 email opt-out
  When preferences 被迁移
  Then product_updates_email 是 disabled。

- Given 用户关闭 push
  When product update 事件触发
  Then 不会为该用户入队 push job。

evidence.md

Required:
- backfill dry-run count
- opt-out preservation test
- duplicate-send regression test
- worker suppression test
- settings UI screenshot
- preference reader flag 回滚方案

评审时应该看什么

第一轮评审看的是当前同意状态有没有被保留。设置页很容易生成,真正危险的是迁移:已有退订、device token、营销 unsubscribe 记录和产品提醒规则看起来很像,只有规格把它们逐一命名后,评审者才能知道谁负责哪一类行为。

第二轮评审看重复投递。迁移过程中,旧 reader 和新 reader 经常会并行运行。如果两边都入队消息,用户就可能对同一个事件收到两条通知。好的规格包要写明双读窗口、事实来源,以及证明只创建一个 job 的回归测试。

第三轮评审看回滚。关闭 UI 并不会撤回已迁移的 preference 行。规格包应该写明控制新 reader 的 flag,以及暂停发布后用于验证 opt-out 仍然保留的查询。

范围检查

SMS、营销重设计、通知历史和后台工具都应拒绝进入当前 diff,除非它们有自己的规格。

证据检查

要求提供 dry-run 数量、退订迁移 fixture、重复发送回归测试,以及一个能和 API 状态对应的 UI 截图。

发布检查

发布应通过 reader flag 控制。停止信号是任何重复产品通知,或 preference API 与 worker 决策不一致。

AI 编码护栏

只实现通知偏好迁移。

允许修改:
- preference schema 和迁移
- settings API 读取/更新
- email worker 抑制
- push enqueue 抑制
- 验收标准对应的测试和 fixture

不要新增:
- SMS consent
- notification history
- admin bulk editor
- marketing email redesign
- 无关 settings page 重构

每个改动文件都必须对应一个任务和一条验收标准。

这条护栏故意写得很窄。通知需求天然会吸引相邻改进:新渠道、更好看的设置页、后台覆盖、活动历史和分析看板。这些可能有价值,但放进同一次迁移会削弱评审,也会让同意状态更难审计。

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

当某个偏好、同意状态或设置变更会影响真实投递行为时,可以套用这个模式。把渠道名称替换成你的业务名称,然后先写旧系统的事实来源,再写新模型。如果当前系统里有 unsubscribe 行、device token、profile flag、团队级设置或地区同意规则,都要逐一列出来。不要让新 UI 成为规格里唯一被描述的东西。

小团队可以把规格包写得很短,但核心内容不能少:当前行为、迁移规则、双读规则、非目标和证据。少了任何一项,AI 实现都可能看起来很完整,却在没有评审的情况下改变同意语义。

大团队还应该加一个“责任归属”部分。产品消息、生命周期营销、账单提醒和移动推送往往有不同 owner。规格没有写清 owner,工程评审可能通过,但另一个团队的投递政策可能已经被破坏。

评审时应拒绝的反模式

只在 UI 判断偏好

worker 必须执行偏好规则。设置开关如果没有进入投递链路,只会制造虚假的安全感。

悄悄改变默认值

改变老用户默认投递行为是产品和同意决策,必须写进规格,而不是藏在迁移脚本里。

回填没有数量校验

没有 dry-run 数量和 mismatch 检查的迁移,无法证明它保留了当前退订状态。

上线后还要观察什么

通知偏好迁移最容易在发布后暴露问题,因为 UI 看起来正确并不等于投递链路真的遵守了偏好。规格包应该预先写好观察窗口。

重复发送

在一个发布周期内观察同一事件是否同时由旧 reader 和新 reader 入队,发现重复 job 就停止 rollout。

退订保留

抽样检查迁移前的退订用户,确认新 preference 行、API 响应和 worker 决策三者一致。

观察窗口结束前,不建议同时上线新的通知渠道或营销规则;否则很难判断问题来自迁移、旧逻辑还是新增范围。

修改通知设置前先套用这个模式

先用规格包生成器起草内容,再把迁移规则和非目标贴进 AI 实现提示词。

编辑说明

这个案例基于通知迁移中常见的失败模式:同意状态漂移、重复发送、缺少回填证据,以及只在 UI 层检查偏好。