物流通知:承运商事件、状态归并与用户触达
物流状态通常来自承运商回调,缺少回调能力时按运单定期查询。用户打开订单页可以请求刷新,但后台同步仍要覆盖长期不登录的用户。
示例只对发货、派送、签收和异常等关键业务变化通知用户,普通轨迹明细保存在查询页,不把每条轨迹都转成一次推送。
从查看轨迹推导后台归并
假设产品只提供“点击查询物流”的工具,按用户操作调用渠道可以成立。增加“发货和签收主动提醒、客服能追溯异常”的要求后,后台需要独立接收或补查,并保存可核对的历史事实。
| 候选方案 | 能提供什么 | 需要承担的成本 |
|---|---|---|
| 页面查询时直接调用渠道 | 当前点击的即时查询 | 未访问用户没有主动同步,突发点击消耗配额 |
| 周期查询所有未完结运单 | 不依赖用户在线 | 查询配额、延迟和终止条件 |
| 持久化回调并配合分层补查 | 实时接续与缺失修复 | 去重、乱序、状态归并和原始证据 |
本篇选择第三种,并把“渠道报告了一条事件”与“本地接受了一次业务转换”分开。通知绑定本地有效转换,才能避免同一来源消息重发时再次通知用户。
回调接收与状态归并时序
接口只有保存原始事件成功后才确认接收。归并 Worker 按当前版本处理,渠道响应成功与本地状态已更新是两个阶段。
签收后迟到的运输中事件与合法退回事件不能用同一条“只取最大状态”规则处理。用户通知以接受的本地转换为身份,再独立进行渠道分发。
物流需求与完成标准
示例覆盖订单拆包、多运输段、异常与退回:用户看到当前确认状态和更新时间,客服能追溯原始轨迹,关键变化生成通知。页面“物流未更新”与“包裹异常”是不同结果。
| 业务问题 | 示例决策 | 开发约束 |
|---|---|---|
| 一单多包何时算发货 | 区分部分发货与全部发货 | 包裹状态与订单汇总分离 |
| 什么事件要通知 | 关键有效转换,普通轨迹只入历史 | 转换 ID 与通知意图关联 |
| 查询渠道不可用怎么办 | 保留最后确认状态,标注更新时间 | 同步状态与物流状态分离 |
| 单号复用怎么办 | 运单绑定承运商账户与运输段身份 | 不单独拿运单字符串当永久主键 |
| 用户投诉怎样定位 | 能找到来源事件、归并规则和发出通知 | 原始数据与处理版本可追踪 |
先保存外部事件,再更新业务状态
| 信息 | 用途 |
|---|---|
| 承运商与运单身份 | 区分来源,处理单号复用或不同运输段 |
| 来源事件 ID | 识别同一事件的重复投递 |
| 发生时间与接收时间 | 分析迟到,而不是只保留一个时间 |
| 原始状态与轨迹 | 支持规则修订和历史重放 |
| 本地状态与版本 | 决定产品展示和后续动作 |
接收回调时校验来源并持久化,再异步处理。没有可靠事件 ID 时,需要根据渠道字段定义去重指纹,并承认指纹碰撞和近似去重的边界。
事件身份可以采用来源与事件 ID 组合的思路;CloudEvents 规范也以 source 与 id 标识事件。具体承运商字段与语义仍以它自己的接口契约为准。
数据结构与接口
| 记录 | 关键字段与约束 |
|---|---|
shipments | 订单、承运商账户、单号、运输段、当前状态、归并版本、最后确认时间 |
shipment_inbox | 运单及运输段关联、来源、事件 ID 或约定指纹、发生与接收时间、原始数据、处理状态 |
shipment_transitions | 本地转换 ID、前后状态、证据、归并规则版本 |
shipment_sync | 下次查询时间、尝试次数、最近成功时间和待核对原因 |
tracking_subscriptions | 运单与运输段、供应商订阅身份、订阅状态、最后确认时间与失败原因 |
shipment_scan_runs | 本轮扫描截止时间、分页游标与推进检查点 |
| 通知记录 | 本地转换 ID、收件对象、渠道与发送结果 |
回调入口验证渠道签名、租户映射和事件格式后持久化,再返回渠道约定的成功响应。重复事件也可返回已接收结果;数据库没有保存成功时,不能提前声称已接收。
用户刷新接口优先返回缓存状态,并按去重规则建立一次补查任务;多个用户同时刷新同一运单,不应直接向渠道发出相同数量的同步查询。
状态归并处理器
领取待处理事件,读取运单当前状态与归并版本
规则能确定合法转换:准备转换候选
规则无法确定:在事务外查询渠道,保存可引用的查询证据
BEGIN
锁定运单,重新检查归并版本是否仍与读取时相同
若已变化:退出本次候选,重新评估,不覆盖新状态
若需要有效转换:
写入 shipment_transition 并推进运单状态与归并版本
为本地转换 ID 保存通知意图
标记原始事件已处理或待进一步核对
COMMIT本地转换 ID 只在真正接受状态变化时产生,重复来源事件不会生成新的转换。渠道查询结果也要记录自己的确认时间与身份,不能把慢查询拿到的旧快照无条件覆盖新回调。
归并版本用于检测“我读取之后,当前事实是否又变化了”。它不判断承运商事件本身的真伪;真实性来自来源校验与渠道契约,合法状态变化来自业务转换规则,三者各有职责。
重复和乱序怎样处理
同一条发货回调收到三次,原始事件去重后只形成一次有效状态变化。签收已经确认后,迟到的普通运输中事件不应直接把页面退回运输中。
但物流并非永远单向前进:拒收、退回、签收纠正和再次派送可能是合法变化。需要显式的状态转换规则和运输段标识,不能只给状态排一个数字然后永远取最大值。
来源有可信版本号时按契约使用;只有时间戳时,不能仅凭本地接收顺序建立事实顺序。冲突事件可以保存后查询承运商当前结果,记录归并依据。
1000 个包裹同时发货
每个运单独立保存状态与通知意图,通知阶段再按用户、订单和时间窗口决定是否汇总。一个订单拆成多个包裹时,要明确“订单已发货”表示部分发货还是全部发货。
发现批次、状态更新批次与渠道发送额度分别控制。第 200 个运单的短信失败,不影响其他运单状态已经更新,也不让整批用户重新收到推送。
通知逻辑键可以采用 shipment_id + normalized_transition_id + channel。同一运输段内相同状态的重复回调不重复发送;再次派送形成的新轮次仍可通知。
用户离线和页面展示
站内通知先保存供下次访问查看,App 推送通过可用设备渠道发送。页面重新读取最新物流状态,消息链接不要把旧轨迹直接当成当前状态。
供应商接受推送不等于用户看到通知。应分别记录通知意图、提交结果与可获得的送达或已读回执。
补查频率与停止条件
| 运单阶段 | 示例策略 |
|---|---|
| 新发货但尚无轨迹 | 在渠道允许范围内等待并补查 |
| 运输中 | 周期更新,按时效和查询配额控制频率 |
| 物流异常 | 建立异常处理任务,必要时通知负责人 |
| 已签收 | 减少主动查询,保留渠道纠正事件的处理能力 |
1000 条运单分散补查时间,避免每轮整点同时打满渠道额度。查询游标、失败次数与最老未更新运单年龄需要可追踪;渠道临时不可用时保留上次确认状态,并显示更新时间。
这里的游标是 shipment_scan_runs 中的批量扫描进度,单运单的下次补查与失败次数属于 shipment_sync,订阅是否停止则属于 tracking_subscriptions。三者分别表示发现进度、同步进度与供应商订阅生命周期,不能用包裹的运输状态代替。
批量补查的检查点
补查扫描固定本轮截止时间,按 next_poll_at + shipment_id 分页发现候选;本页补查任务可靠保存后才推进扫描检查点,独立任务再调用渠道。若任务创建与检查点不能同事务提交,按稳定任务业务键重放本页,不能先推进游标再建任务。
单运单的下一次查询时间、失败原因与本次确认结果一起提交;有新的有效回调时可以重新计算补查计划。next_poll_at 是可变排序键,本轮分页不代替后续重新扫描,扫描推进记录也不代表所有运单已经同步完成。
对已签收、已退回等状态设置不同的查询策略与最终复核窗口,而不是永久无限轮询。未解决的异常进入客服待办;渠道配额不足以满足承诺时,显示同步延迟并告警。
一次乱序回调与补查竞争
假设运单 S5 当前为运输中,归并版本为 10:
| 动作 | 本地记录 | 应采取的判断 |
|---|---|---|
| 补查读取版本 10 后请求渠道 | 尚未发布查询结果 | 外部调用不持有长事务 |
| 新签收回调先到并确认 | 保存转换 T11,归并版本推进为 11 | 保存签收通知意图 |
| 较慢的补查返回运输中 | 请求依据仍是版本 10 | 检查冲突后重新核对,不覆盖签收 |
| 相同签收回调重发 | 命中已有来源事件 | 不再生成一份 T11 通知 |
| 后续收到合法退回证据 | 保存原始事件并按运输段规则评估 | 可以产生新转换,不靠状态数字大小拒绝 |
如果仅把每次收到的 status 覆盖到运单表,就无法区分迟到旧状态和合法纠正。保留原始证据和归并结果,可以在规则修订时重放核对,而不篡改当时收到的事实。
验收与排查依据
| 场景 | 应断言的结果 |
|---|---|
| 同一来源事件重发 10 次 | 原始身份去重,只有一次有效转换和通知意图 |
| 签收后收到迟到运输事件 | 不回退;原始事件仍可追溯 |
| 签收后收到有效退回事件 | 按运输段规则处理,不因状态数字较小而丢弃 |
| 页面 100 次刷新同一运单 | 补查请求合并,不打出 100 次渠道查询 |
| 渠道慢查询与新回调竞争 | 归并版本阻止旧查询覆盖新状态 |
| 一单三个包裹仅一个发货 | 页面和通知按部分发货规则展示 |
观察原始事件滞留、待核对事件年龄、最后确认状态年龄和渠道限流。告警应能指向运单归并证据,而不把每次接口波动都发给用户。
快递100接入说明中的订阅状态
腾讯云开发者社区里,“快递100API”发布的接口问题说明给出了一个具体边界:停止跟踪的通知也需要返回协议要求的成功响应,否则会被重推;重开订阅、重推上次内容和关闭跟踪是不同操作。这是 2021 年的供应商接入经验,不能作为腾讯云自身物流架构或当前所有接口参数的说明。
本文据此建议把 tracking_subscription_status 与包裹的 shipment_status 分开保存。“停止跟踪”不证明包裹已经签收或丢失,回调处理成功也只证明消息已接收。先持久化接收记录,再返回确认,之后由归并任务判断是否更新轨迹、继续补查或进入人工异常处理。
如果 1000 个单号同时出现订阅异常,应按原因分类修复并限制重开速率,避免无条件重订阅形成调用风暴。历史文中的重试次数和时间不能硬编码为通用规则;上线时需按所接服务的现行协议核对签名、确认报文、重试及订阅终止条件,本文原有的来源校验仍须保留。
美团即时物流:业务分区与变更同步
美团的即时物流分布式系统架构文章讨论了分区扩容、按区域或城市路由,以及通过数据库变更同步支撑其他存储的最终一致性。这是物流平台的整体架构经验,并未公开本文所用的承运商回调归并算法。
对物流通知的启发是:同一运单的回调、补查和重放,应能定位到当前负责它的业务分区;路由或容灾切换后不能让新旧分区各自建立一份有效通知。可以将稳定运单身份、路由信息与归并版本一并记录。业务状态落库后若还有缓存、查询索引或通知副本,需分别定义同步进度与恢复方式,不能把回调接口成功返回当作所有副本已经更新。
阿里云 EventBridge:重试耗尽后如何保留待处理轨迹
EventBridge 的事件流重试和死信说明区分重试策略与异常容错行为:按配置,失败可能进入死信、被丢弃或阻塞任务;死信并非默认启用。这是事件流产品的具体行为,不应套用于所有消息队列。
若采用事件流转发物流轨迹,应明确坏消息能否阻塞后续轨迹、重试耗尽后谁处理,以及如何从原始接收记录重新投递。本文建议将无法识别的承运商状态隔离并告警;重放时沿用原事件身份,经当前归并规则判断,不能重新制造发货或签收通知。运单本身的运输异常与技术投递死信需要分别运营。
参考资料
美团技术团队:美团即时物流的分布式系统架构设计
阿里云官方文档:EventBridge 事件流:重试和死信
快递100API,腾讯云开发者社区:调用快递100查询类接口出现错误时的解决方法
CloudEvents:CloudEvents Specification:事件身份与属性
AWS Prescriptive Guidance:Transactional outbox pattern