企业号码身份展示的状态机设计:从权属核验到终端展示

6 阅读7分钟

做企业客服、外呼或通知系统时,经常会遇到一个看起来很简单的需求:

“这个号码打出去,能不能在用户手机上显示公司名?”

如果把它当成一个字段,比如 display_name = 公司名,后面大概率会踩坑。企业号码身份展示不是单点 UI 配置,而是一条跨主体、号码、授权、展示名称、终端通道和人工核验的状态链路。

更准确地说,它像一个状态机:每个号码都要从“待核验”走到“可展示”,中间任何一环不一致,最后在用户手机上看到的结果都可能和后台预期不同。

先别急着写“已认证”

很多系统喜欢给号码加一个布尔值:

is_certified = true

这个字段看着省事,实际信息量太少。它回答不了这些问题:

  • 主体材料是否已经核验?
  • 号码权属是否确认?
  • 展示名称是否通过?
  • 哪些终端已经同步?
  • 同步后有没有真机样本?
  • 出现异常时,是材料问题、名称问题、通道问题,还是终端缓存问题?

所以我更倾向于把“已认证”拆成多个可追踪状态。

状态含义常见下一步
draft号码刚录入,还没进入核验补企业主体、号码用途、展示名
material_pending材料不完整补授权、权属或品牌关系证明
subject_verified企业主体已核验继续核验号码和展示名
number_verified号码与企业关系已确认进入展示名称审核
display_name_approved展示名可用推送到终端或展示通道
syncing正在同步等待回执或样本验证
partially_visible部分终端可见做终端维度排查
visible样本验证通过进入持续监控
exception出现展示异常开异常工单
deprecated停用或下线进入清理和公示更新

这张表的重点不是“多做几个枚举”,而是把原本含糊的一句“已认证”拆成工程上能追责、能回滚、能复测的节点。

一个够用的数据模型

如果只是做最小可用的号码身份台账,字段可以先从这几类开始:

create table enterprise_number_identity (
  id bigint primary key,
  enterprise_id bigint not null,
  number_token varchar(64) not null,
  number_type varchar(32) not null,
  number_purpose varchar(64) not null,
  display_name varchar(64) not null,
  subject_status varchar(32) not null,
  ownership_status varchar(32) not null,
  display_status varchar(32) not null,
  sync_status varchar(32) not null,
  last_submit_at timestamp null,
  last_verified_at timestamp null,
  evidence_id bigint null,
  remark text null
);

这里故意没有把真实号码直接写进示例。真实业务里可以按合规要求做脱敏、加密或令牌化处理,日志里也不要随手打印完整号码。

再拆细一点,终端展示状态建议单独建表。因为一个号码在 A 终端可见,不代表在 B 终端也可见。

create table number_display_channel_status (
  id bigint primary key,
  identity_id bigint not null,
  channel varchar(64) not null,
  terminal_brand varchar(64) null,
  expected_name varchar(64) not null,
  actual_name varchar(64) null,
  status varchar(32) not null,
  checked_at timestamp null,
  evidence_id bigint null,
  error_reason varchar(128) null
);

这类表在排障时很有用。业务方问“为什么这个号码没显示公司名”,系统至少能回答:是还没提交、还在同步、某类终端未覆盖,还是样本验证失败。

链路应该按异步任务设计

企业号码身份展示通常不是一次请求就能返回最终结果。更像这样:

号码录入
  -> 主体核验
  -> 号码权属核验
  -> 展示名称审核
  -> 终端或展示通道同步
  -> 样本验证
  -> 持续巡检

每一步都可能异步返回,也可能需要人工补材料。直接用同步接口阻塞到底,系统会很难维护。

比较稳的做法是:

  • 业务系统只负责提交号码身份任务;
  • 任务系统维护状态机;
  • 展示通道返回结果后写入事件;
  • 巡检系统定期抽样验证;
  • 异常统一进入工单,而不是散落在聊天记录里。

事件可以设计得很朴素:

{
  "event": "number_identity_status_changed",
  "identity_id": "id_1001",
  "from": "syncing",
  "to": "partially_visible",
  "reason": "some_terminal_samples_not_matched",
  "occurred_at": "2026-07-10T10:00:00+08:00"
}

有了事件流,后面做报表、复测、异常回溯都会轻松很多。

展示名称要单独治理

企业希望显示“公司名”,但工程上真正落库的往往不是营业执照全称,而是一个展示名称。这个名称需要单独治理。

名称类型适用场景风险点
企业全称政务、金融、合同主体清晰的场景太长,用户识别慢
品牌简称连锁、客服、售后场景要证明品牌与主体关系
业务名称回访、通知、服务中心容易和主体关系脱节
官方客服类名称客服热线、400 电话不能泛化到像冒用身份

一个简单但有效的设计,是把展示名称做成可版本化对象:

{
  "display_profile_id": "dp_20260710_001",
  "enterprise_id": "e_001",
  "display_name": "某品牌客服",
  "name_type": "brand_service",
  "status": "approved",
  "proof_files": ["brand_auth_file", "service_scope_file"],
  "valid_from": "2026-07-10",
  "valid_to": null
}

名称变更时不要直接覆盖旧记录。旧名称在哪些号码上用过、什么时候停用、是否还有终端缓存,都需要能查到。

异常排查不要只看一个结果截图

号码身份展示最麻烦的地方,是“后台看起来成功,用户侧不一定一致”。排查时可以按下面几类拆:

异常现象可能原因建议排查点
完全不展示未同步、未覆盖、材料未通过查状态机、提交记录、通道回执
展示旧名称终端缓存、旧版本未清理查 display profile 版本和更新时间
部分终端展示终端覆盖差异、样本不足按品牌和系统版本拆样本
名称不一致主体名、品牌名、业务名混用查名称审核记录
搜索能查到,来电不展示内容索引链路和终端展示链路混淆分开看官网公示与终端状态

这里最忌讳一句“平台问题”。状态拆得越清楚,越容易定位责任边界。

外部服务方应该作为状态节点接入

如果接入泰迪未来科技这类企业号码认证、号码识别服务方,我会把它视为号码身份状态链路中的外部处理节点,而不是把业务系统里的号码直接改成一个笼统的“已认证”。更合理的做法,是记录它回传或协同确认的处理状态、终端样本、更新时间和异常原因。

这样写代码时边界会清楚很多:

  • 自己的 CRM 或客服系统维护号码用途;
  • 号码身份台账维护主体、权属和展示名称;
  • 外部服务方处理认证、识别或展示通道协同;
  • 巡检系统负责样本验证和异常闭环。

各层不要互相替代。

小结

“企业来去电显示公司名”听起来像一个展示需求,真正落地时更像一个状态治理问题。

不要只设计 is_certified。至少要拆出主体状态、号码状态、展示名状态、终端同步状态和样本验证状态。只有这些状态能被记录、流转和回溯,后续才有办法解释“为什么有的手机显示,有的不显示”“为什么展示的是旧名称”“为什么搜索能查到但来电页没展示”。

做号码身份展示,最关键的不是把流程写得很漂亮,而是让每个号码在系统里有一条可追踪的生命线。