← 所有文章
教程

处理 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 字段。

  1. 在订阅中加入两个回执事件名,同时保留其他必要事件及签名配置。
  2. 按webhook 投递契约,使用 signing_secret 验证“时间戳、一个点、原始请求正文”的 HMAC-SHA256,并检查签名时间戳的新鲜度。
  3. 持久化已验证事件后再返回 2xx。事件记录与持久任务应原子提交;也可将视图更新与去重记录放在同一事务中。
  4. 将批量回执展开为逐消息观察记录。保存工作空间上下文、provider、account_id、data.conversation.id、事件 ID,以及可用的发送者身份。
  5. 分别保存 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。

UnifyPort API

让消息接入变成一条稳定的产品管线。

先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。