处理 UnifyPort 批量送达与已读回执
一条 UnifyPort 送达或已读回执可能涉及多条消息。存在批量字段时,应处理 data.message.ids 中的每个 ID;单条回执使用 data.message.id。把回执与消息正文分开存储,即使回执先到也不会丢失。发送请求被接受、收件人回执,以及团队收件箱的已读状态,是三种不同证据,不能共用一个“成功”标记。
核心要点
- 按工作空间、渠道、消息账号和会话匹配回执,不能只用消息 ID。
- 渠道报告的回执时间与 webhook 到达时间分别保存。
- 晚到的送达回执不能抹掉已经观察到的已读证据。
- 没有确认意味着未知,不代表可以重新发送。
先确认批量范围,再更新界面
标准事件契约将 message.delivered 定义为消息送达收件人的设备,将 message.read 定义为收件人已读回执。批量字段是 data.message.ids;只更新单数 id 会漏掉其余消息。
| 证据 | 能确认什么 | 不能确认什么 |
|---|---|---|
发送响应 status: accepted | 请求已被接受 | 送达或已读 |
message.delivered | 指定消息已送达 | 收件人已阅读 |
message.read | 指定消息已读 | 工单已解决或所有群成员已读 |
| 会话本地已读状态 | 已连接账号的聊天列表状态 | 收件人的回执 |
最后一项属于独立的共享收件箱已读/未读流程。不要调用会话已读操作来修复缺失的外发消息回执。
以下是依据已读回执文档缩减的说明性示例,不是真实客户事件:
{
"id": "evt_2f9c1a4b7e",
"type": "message.read",
"provider": "whatsapp",
"account_id": "acc_8c21d0",
"occurred_at": "2026-06-08T12:35:10Z",
"data": {
"conversation": { "id": "8613912345678", "type": "user" },
"sender": { "id": "8613912345678", "type": "user" },
"message": {
"id": "wamid.HBgM",
"ids": ["wamid.HBgM", "wamid.HBgN"],
"read_at": "2026-06-08T12:35:10Z"
}
}
}
该回执覆盖列表中的两条消息,并不证明更早的消息都已读。即使 ID 看起来是数字,也不能自行推导一个连续范围。
先保存观察记录,再生成状态标记
建议分为两层:持久化的已验签事件记录,以及按消息组织的回执视图。这是应用存储设计,不是新增的 UnifyPort 字段。
- 在订阅中加入两个回执事件名,同时保留其他必要事件及签名配置。
- 按webhook 投递契约,使用
signing_secret验证“时间戳、一个点、原始请求正文”的 HMAC-SHA256,并检查签名时间戳的新鲜度。 - 持久化已验证事件后再返回 2xx。事件记录与持久任务应原子提交;也可将视图更新与去重记录放在同一事务中。
- 将批量回执展开为逐消息观察记录。保存工作空间上下文、
provider、account_id、data.conversation.id、事件 ID,以及可用的发送者身份。 - 分别保存
occurred_at、渠道的read_at或delivered_at,以及应用自己的到达时间。回执时间也会镜像到data层的同名字段。缺失时间保持未知,不能用到达时间冒充渠道时间。
下面的 JavaScript 只从已经验签的事件中提取消息范围,不是完整接收器或数据库实现:
function receiptMessageIds(event) {
if (!['message.delivered', 'message.read'].includes(event.type)) return [];
const message = event.data?.message;
const ids = message?.ids === undefined ? [message?.id] : message.ids;
if (!Array.isArray(ids) || ids.length === 0 ||
ids.some(id => typeof id !== 'string' || id.length === 0)) {
throw new Error('Receipt scope needs review');
}
return [...new Set(ids)];
}
批量字段不合法时,不要悄悄退回单数 ID。保留事件供排查,不要按猜测的范围更新。
合并证据,不制造送达结论
独立保留送达与已读观察。已读回执先于送达回执到达时,可以显示“已观察到已读”,但不要编造 delivered_at。随后到达的送达事件只补充证据,不能把界面降回“已送达”。重复事件也不应重复触发通知。
若消息正文尚未入库,按完整消息身份保留回执,等待匹配记录出现。不能仅凭回执创建入站消息或增加收件箱计数。可以用 occurred_at 和事件 ID 建立确定性顺序,但不能采用“最后到达的状态覆盖一切”的逻辑。
发送时,将响应中的 message_id 和 provider_ref 与本地操作一起保存。关联回执前,确认哪个标识对应实际观察到的渠道消息;字段名称不同,并不意味着它们总是相等。无法关联的回执应进入核对队列,不能按时间就近挂到某条消息上。保留可用的发送者身份,但不要把一次观察扩展为“所有人已读”。
媒体发送排查指南处理的是来源文件及请求接受环节的问题。回执处理发生在另一层,超时或没有回执都不能自动触发再次发送。
能力边界与验收场景
UnifyPort 使用非官方接口。渠道事件矩阵描述解析器映射,不保证每个账号或部署都产生回执。在界面承诺送达标记前,应核对所选渠道,包括 LINE。没有回执不能证明未读、未送达或被屏蔽;遗漏的事件也没有保证重放。
建议测试:覆盖两条消息的批量回执、单条回执、重复投递、已读先于送达、回执先于正文、不同账号使用相同消息 ID、无效批量字段,以及始终没有回执。确认只有明确列出的 ID 被更新,且没有任何场景自动重发。这些是待执行测试,不是已取得的测试结果。
常见问题
应处理 id 还是 ids?
存在有效且非空的批量 ids 时使用它;否则使用单数 id。无效范围应进入排查流程。
收到已读回执后,可以补填 delivered_at 吗?
不可以。展示已读证据即可,不要制造渠道送达时间。
没有回执能把发送标记为失败吗?
单凭这一点不能。保留“确认未知”,检查渠道能力、订阅、接收链路及标识匹配。
下一步与参考资料
从送达回执文档开始,先实现有明确范围的观察记录存储,再添加界面状态。
产品资料核对日期:2026-11-06。
让消息接入变成一条稳定的产品管线。
先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。