轨迹归一处理的是这样一个问题:三条承运商对「揽收」这一个动作,分别写作「快件已被快递员揽收扫描」「收件」「【已揽收】广州转运中心」。这些文案写给官网访客看,措辞随网点与系统版本变化,没有稳定性承诺。系统要消费这些数据,就必须有一层归一;而归一表的格子怎么设,是一个被普遍低估的决策。
常见的三类错法:就近归类(见「运输」二字即归运输中,忽略前置的「投柜」「代收」);漏归即丢(表外新文案被静默丢弃,用户轨迹凭空缺一截);错把过程当终点(「到达派件城市」被归成签收,客服提前答复「已送达」)。
五列表:每一列都是一个明确责任
| 上游原文模式(示例) | 归一态 | 是否终态 | 允许的变化方向 | 前端展示 / 客服话术 |
| 已揽收扫描 / 收件 | 已揽收 | 否 | →运输中 | 「快递已揽收」 |
| 到达转运中心 / 干线 | 运输中 | 否 | →派送中 | 「包裹运输中」 |
| 到达派件城市 / 派件 | 派送中 | 否 | →签收 | 「派送员正在派送」 |
| 已放置快递柜 / 已交付代收点 | 已投柜 | 是 | 仅取出签收 | 「已放至柜,取件码XXXX」 |
| 本人签收 / 代签 | 已签收 | 是 | 无 | 「已签收」 |
| 滞留 / 破损 / 超时未更新 | 异常 | 否 | 按处理结果流转 | 「包裹异常,客服跟进中」 |
| 表内无命中 | 未知 | 否 | 原样展示 | 上游原文 |
两个关键设计:其一,「已投柜」独立成格且按终态处理——它归进运输中会产生误催单,归进签收会让用户找不到取件码,它是字典里唯一「终态但有例外出口」的格子。其二,「未知」兜底格必须存在,归一失败的节点原样展示,不丢、不猜。
实现与运行期机制
RULES = [ (("已放置快递柜", "已交付代收点", "已存入柜"), "DEPOSITED"), (("代签", "本人签收", "已签收"), "SIGNED"), (("超时未更新", "滞留", "破损"), "ABNORMAL"), (("退回", "无法送达"), "RETURNING"), (("派件", "到达派件城市"), "DELIVERING"), (("转运中心", "运输中", "干线"), "TRANSIT"), (("揽收", "收件"), "PICKED_UP"), ] def normalize(raw: str) -> str: if not raw: return "UNKNOWN" for patterns, state in RULES: for p in patterns: if p in raw: return state return "UNKNOWN" # 命不中不丢、不猜,落未知格
规则按长模式优先排列,避免短模式在终态文案中途抢先命中。运行期还有两条:展示前按节点时间重排,不依赖上游数组顺序(正序倒序随上游定,时间精度不齐按天粒度稳定排序);归一表带版本注释(修订日期 + 触发文案样例),上游改措辞时可回溯。
体检指标
每周统计未知态节点占比:稳定低于百分之一是常态;某周突然抬升,几乎一定是上游改了文案,归一表应随之发版——这个指标让表的腐化在客诉出现之前被发现。
验收标准
- 「已放置于快递柜」节点应归入已投柜,不进入客服异常清单。
- 表外新文案应落未知态,原文完整展示,未知态计数加一。
- 先「已签收」后「运输中」:后者只入库不回写展示状态。
- 轨迹数组倒序传入:展示顺序与正序传入一致。