Push & Message:推送编排与消息历史
推送系统最容易被误解成“调用一次微信 API 就结束”。
但如果你真的把它当成“发一下消息”,你会很快遇到三个现实:
- 目标用户可能不止一个(subscribe 模式)
- 外部 API 会失败,而且失败原因各不相同
- 你需要能回答“刚才到底发生了什么”(否则排障会很痛苦)
因此 push.service 的核心价值不是“发消息”,而是编排与可观测。
推送核心编排在:
node-functions/services/push.service.ts
输入极简,内部复杂:这是编排服务该有的样子
push 的输入非常简单:
appKey{ title, desp }
但内部会把这个极简输入翻译成一条完整的执行链路。
一张流程图:push.service 做了哪些决定
flowchart TD
A["/send 接口
收到 appKey + message/"] --> B[生成 pushId] B --> C[按 appKey 找 App] C --> D[列出 App 绑定的 OpenIDs] D --> E[读取 Channel 配置] E --> F{pushMode?} F -->|single| G[取第一个 OpenID] F -->"|"subscribe"|" H[取全部 OpenID] G --> I{messageType?} H --> I I -->"|"template"|" J[sendTemplateMessage] I -->"|"normal"| K[sendCustomMessage] J --> L[汇总 DeliveryResult] K --> L L --> M[写入 Message 历史] M --> N[返回 PushResult]
收到 appKey + message/"] --> B[生成 pushId] B --> C[按 appKey 找 App] C --> D[列出 App 绑定的 OpenIDs] D --> E[读取 Channel 配置] E --> F{pushMode?} F -->|single| G[取第一个 OpenID] F -->"|"subscribe"|" H[取全部 OpenID] G --> I{messageType?} H --> I I -->"|"template"|" J[sendTemplateMessage] I -->"|"normal"| K[sendCustomMessage] J --> L[汇总 DeliveryResult] K --> L L --> M[写入 Message 历史] M --> N[返回 PushResult]
你会看到这里的“边界”很清楚:
- push.service 负责决策与编排
- 平台 service(wechat/enterprise)负责具体发送与 token 维护
- message.service 负责历史落库
为什么一定要落 Message 历史
推送这类系统,最重要的不是“成功率”,而是“可解释性”。
当你收到用户反馈:
你这个推送没收到。
你必须能回答:
- 是没有目标用户(没绑定)?
- 是 token 失效(自愈失败)?
- 是用户未关注(不可自愈)?
- 是网络问题(可重试)?
因此 message history 不是“日志”,它是产品能力的一部分。
DeliveryResult:失败语义必须结构化
推送是“批处理投递”,所以返回结构更应该是:
- 每个接收者的 success/error/msgId
而不是一个 boolean。
这带来的直接收益:
- UI 可以展示“谁失败了、为什么失败”
- 后续可以做“只重试失败的那部分”
失败分类:可自愈 vs 不可自愈
项目里典型失败分类:
- token 失效(可强制刷新并重试一次)
- 用户未关注(微信错误码 45015 等,不可自愈)
- 网络请求失败(可重试,但要避免重放风暴)
建议 UI 层把这些分类清晰呈现,否则用户会把所有失败都归因到“系统不稳定”。
启发:编排服务的价值在“把复杂留在内部”
- 对外:输入越简单越好(title/desp 足够)
- 对内:需要结构化结果、历史落库、明确责任边界
- 对未来:有了 message history,你就拥有了演进空间(限流、重试队列、告警)
