
系列第 15 篇:把喵提醒放在业务事务之外,严格区分第三方接受、明确失败和结果未知,并用限频摘要避免提醒轰炸。
候选、交换和截止事件已经写进数据库,接下来才轮到外部提醒。这个顺序非常重要:第三方接口变慢、停服或喵码失效,都不能让用户刚刚发布的信息或已经形成的交换消失。
1. HTTP 200 不代表成功
Gate A 实测发现,成功、参数错误和提醒单不存在都返回 HTTP 200 与纯文本。K7 的适配器因此同时判断两层:
2xx + 正文严格等于“完成” → 第三方已接受
2xx + 参数格式有误/找不到提醒单 → 明确拒绝
5xx → 第三方失败
超时、断连、未知正文 → 结果未知
页面显示“第三方已接受”,不显示“微信已送达”。只有接收端真正确认才能证明送达,接口响应做不到。
2. 结果未知为什么不能重试
超时时,服务端可能已经收到并处理请求,只是响应没有回来。喵提醒也没有提供调用方可见的幂等键或结果查询接口。
若自动重试,用户可能收到两条完全相同的提醒。Gate A 已经实际证明相同请求会重复触发。因此 UNKNOWN 是终态:记录下来、保留站内提醒,但不自动发送第二次。
3. 业务事实先于渠道结果
数据关系是:
业务事件
├─ 接收者 A 的站内提醒
├─ 接收者 A 的外部投递结果
├─ 接收者 B 的站内提醒
└─ 接收者 B 的外部投递结果
Worker 只修改某一条投递记录。测试让第一个接收者明确失败,十秒后第二个接收者成功,最终两个状态分别保留,候选和双方站内提醒都没有回滚。
没有启用渠道时,投递标为“仅站内”,用户仍能在提醒中心看到事实。
4. 喵码只在发送瞬间解密
数据库保存版本化密文。Worker 取得租约后在后端解密,只把喵码交给渠道适配器。浏览器、分享文案、业务事件 payload 和普通日志都不包含喵码。
正式正文也固定为:
发现新的同路信息,请返回邻行查看
司机或乘客身份、路线、时间和微信号都留在登录后的邻行页面,不发送给第三方。
设置页的“发送测试提醒”改为使用同一渠道工厂:测试环境使用假渠道,生产配置为喵提醒时走同一适配器。明确拒绝不会把账号标为已验证,更不会删除账号;页面还会提示用户确认微信端实际收到。
5. 数据库租约和十秒间隔
待办领取使用短事务、select_for_update(skip_locked) 和一分钟租约。外部网络调用发生在领取事务之后,不长期占用数据库锁。
Worker 查询最近一次实际调用时间,下一次不足十秒时只调整计划时间,不发送。这个间隔是出口全局约束,而不只是单个用户约束,符合 Gate A 的官方建议。
6. 第四条开始合并摘要
同一用户、同一条出行信息在滚动一小时内最多发送三条即时提醒。第四条不会立刻请求第三方,而是计划到窗口结束后发送摘要;后续事件合并到同一个摘要并增加计数。
注入时钟测试在 0、10、20 秒发送前三条,30 秒处理第四条时没有调用渠道;第五条被标记为已合并,待发送摘要计数变成 2。
这不是删除事件。每个站内提醒仍然存在,只是外部渠道把多个提示合成一次。
7. 本节点结果
111 个非浏览器测试通过,1 个 PostgreSQL 专属测试跳过; 分支覆盖率 88.09%; 静态、类型、迁移和 Django 检查通过; 4 条 Chromium 旅程通过; 成功、拒绝、500、超时、断连、未知正文、故障隔离、十秒间隔和摘要均有自动证据; Gate A 的双用户真实送达记录继续作为外部证据,本节点没有保存测试喵码; WebKit、微信成品真机和正式试用仍未宣称通过; 结果只形成本地提交,不推送。
下一篇进入 K8:用户申请删除后怎样立即停止新披露,在 24 小时、90 天和 180 天三个时间边界清理不同数据,同时让社区管理员只能使用被允许的管理能力。
8. 关键代码与操作
下面的参数化测试展示为什么不能只检查 HTTP 状态码,还必须对白名单正文做严格分类:
@pytest.mark.parametrize(
("status_code", "body", "expected"),
[
(200, "完成", DeliveryStatus.DELIVERED),
(200, "发送失败:参数格式有误", DeliveryStatus.REJECTED),
(500, "service failed", DeliveryStatus.FAILED),
(200, "unexpected", DeliveryStatus.UNKNOWN),
],
)
deftest_response_requires_http_and_whitelisted_body(status_code, body, expected):
channel = MiaotixingChannel(
transport=lambda url, timeout: TransportResponse(status_code, body)
)
outcome = channel.send(
NotificationMessage("example-code", "candidate", "event:1", "最小正文")
)
assert outcome.status is expected
验证命令:make bugfix TEST=tests/notifications/test_miaotixing_channel.py::test_response_requires_http_and_whitelisted_body
示例隐藏了真实提醒凭据和网络请求;超时与断连还要单独断言为 UNKNOWN。
9. 本篇验证摘要
业务事件和站内提醒先提交,第三方调用失败不会回滚产品事实; 渠道结果严格区分已接受、明确拒绝和结果未知,未知结果不自动重试; Worker 使用数据库租约领取任务,网络调用不会长期占用业务事务; 全局十秒间隔和一小时摘要限制外部提醒频率,但不删除站内事件; 测试覆盖成功、拒绝、500、超时、断连和未知正文,真实页面只声称第三方已接受。
10. 附录:相关工具与仓库
10.1 gstack
仓库:garrytan/gstack 地址:https://github.com/garrytan/gstack
10.2 dev-harness
仓库:Dev-Wiki/dev-harness 地址:https://github.com/Dev-Wiki/dev-harness
10.3 UI UX Pro Max Skill
仓库:nextlevelbuilder/ui-ux-pro-max-skill 地址:https://github.com/nextlevelbuilder/ui-ux-pro-max-skill
夜雨聆风