乐于分享
好东西不私藏

AI 软件开发实战教程(十五):第三方提醒失败也不能丢业务事实

AI 软件开发实战教程(十五):第三方提醒失败也不能丢业务事实

系列第 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