手机归属地查询接进 CRM:来电弹屏显示归属地与运营商
约 13 分钟 进阶手机号码归属地查询CRM 集成来电弹屏客服系统
# 手机归属地查询接进 CRM:来电弹屏显示归属地与运营商
接口:手机归属地查询(apiCode=6,接入点 6-1)|是否免费:是,注册后默认可调用,设使用档次限制|请求方式:POST / GET|返回格式:JSON|适用人群:做客服系统、呼叫中心、CRM 的开发者与产品经理|阅读时间:约 8 分钟|最后实测核对:2026-09-18
来电弹屏要在一两秒内把客户信息推到坐席屏幕上,归属地和运营商是其中最容易补上的两个字段:号码是从网关拿到的,直接查一次就能显示「北京 · 移动」。这件事的难点不在查询本身,在于查询不能拖慢弹屏,也不能因为查询失败就让坐席看到一片空白。
## 核心要点
- 归属地查询放在弹屏链路里,加短超时并设降级分支,查询失败不阻断弹屏。
- 同一客户会反复来电,缓存键用前 7 位号段,重复来电基本都能命中。
- `type` 用于话术分流,`prov`、`city` 用于地域分组和排班,两者用途不同。
## 弹屏链路怎么摆
来电事件的链路是:网关上报号码,CRM 收到号码后查客户库,同时查归属地,最后把结果推给坐席界面。归属地查询是链路里唯一的外部依赖,要把它放在不阻塞主流程的位置。
```python
import re
import requests
import redis
import json
API_URL = "https://route.showapi.com/6-1"
APP_KEY = "YOUR_APPKEY" # 正式环境从配置或密钥管理服务读取
r = redis.Redis(decode_responses=True)
CARRIER = {1: "移动", 2: "电信", 3: "联通", 4: "广电", -1: "未知"}
def to_segment(phone: str) -> str | None:
cleaned = re.sub(r"[-\s]", "", (phone or "").strip())
if not re.fullmatch(r"\d{7,}", cleaned) or cleaned[0] == "0":
return None
return cleaned[:7]
def locate_for_popup(phone: str, timeout: float = 1.5) -> dict:
"""弹屏用查询:短超时 + 缓存 + 降级,任何异常都返回可展示的默认值。"""
seg = to_segment(phone)
if not seg:
return {"available": False, "reason": "format"}
key = f"phone:seg:{seg}"
cached = r.get(key)
if cached:
rec = json.loads(cached)
if rec.get("ok"):
return {"available": True, **rec["data"]}
return {"available": False, "reason": "not_found"}
try:
resp = requests.get(
API_URL,
params={"appKey": APP_KEY, "num": seg},
timeout=timeout, # 弹屏链路用更短的超时,不让坐席等
)
body = resp.json().get("showapi_res_body", {})
except requests.RequestException:
# 网络异常时直接降级,坐席界面照常弹出,只是归属地留空
return {"available": False, "reason": "timeout"}
if body.get("ret_code") != 0:
r.setex(key, 600, json.dumps({"ok": False}))
return {"available": False, "reason": "not_found", "ret_code": body.get("ret_code")}
data = {
"prov": body.get("prov"),
"city": body.get("city"),
"carrier": CARRIER.get(body.get("type"), "未知"),
"carrier_type": body.get("type"),
"raw_name": body.get("name"),
"area_code": body.get("areaCode"),
"prov_code": body.get("provCode"),
}
r.setex(key, 30 * 24 * 3600, json.dumps({"ok": True, "data": data}, ensure_ascii=False))
return {"available": True, **data}
```
三个设计点。第一,超时设成 1.5 秒,比官方标注的 10 秒短得多,因为弹屏链路对延迟敏感,超过 1.5 秒还拿不到结果就不值得等。第二,异常分支一律返回带 `available: False` 的默认值,调用方不用写 `try`。第三,成功结果缓存 30 天,失败结果只缓存 10 分钟,避免暂时的失败被长期沿用。
完整的缓存实现见[按前 7 位号段做缓存](https://www.showapi.com/guides/phone-attribution-cache-6)。
## 用 type 做话术分流
坐席话术常常按运营商分家,比如某些业务对特定运营商的号码有专属方案。`type` 的取值是稳定的数字枚举,适合直接做分流条件。
```python
SCRIPT_BY_CARRIER = {
1: "mobile-playbook", # 移动号码话术
2: "telecom-playbook", # 电信号码话术
3: "unicom-playbook", # 联通号码话术
4: "cbn-playbook", # 广电号码话术
}
def pick_script(location: dict) -> str:
if not location.get("available"):
return "default-playbook" # 归属地不可用时走通用话术
return SCRIPT_BY_CARRIER.get(location.get("carrier_type"), "default-playbook")
```
判断运营商用 `type`,不要用返回的 `name`。实测里同一家运营商在不同号段返回的 `name` 不一样,`1380013` 返回「移动全球通卡」,`1970000` 只返回「移动」。原因和枚举对照见 [type 字段全解](https://www.showapi.com/guides/phone-attribution-type-field-6)。
## 推送到坐席界面
查询结果通过 WebSocket 或 SSE 推给前端。消息体只带界面需要的字段,避免把整份返回塞过去。
```javascript
// 前端:收到弹屏事件,渲染归属地
socket.onmessage = (event) => {
const payload = JSON.parse(event.data);
if (payload.type !== "call-popup") return;
const loc = payload.location || {};
locationEl.textContent = loc.available
? `${loc.prov} ${loc.city} · ${loc.carrier}`
: ""; // 不可用时留空,不显示错误文案给坐席
if (loc.available && loc.carrier_type === 2) {
scriptPanel.load("telecom-playbook"); // 按运营商加载话术
}
};
```
坐席界面上不展示查询失败的原因,只留空白。失败信息记到日志里,由技术人员排查。坐席看到「查询失败」除了增加疑问,没有别的用处。
## 地域分组与排班
`prov` 和 `city` 是文字,直接用于展示。要做分组统计或排班,用 `cityCode`。「城市编码,本城市身份证的前几位编码」,实测 `13800138000` 落在北京时返回 `110100`,跟北京的城市编码一致。按 `cityCode` 分组比按 `city` 文字分组稳定,省掉了「北京」和「北京市」这种写法差异带来的问题。
地域维度只能到市一级。文档对 `city` 的定义就是「市」,需要区县维度时得用其他数据源补。
## 调用量的一点说明
弹屏场景一天内的不同号段数量通常远小于来电次数,缓存命中率比较高。档位上限按免费档位说明执行,从 L0 的 100 次/天起,可以通过平台积分升级。具体档位与并发上限见[免费接口与档位说明](https://www.showapi.com/free-api)。
失败时 `showapi_fee_num` 为 0,不消耗额度。这条在弹屏这种高频触发场景里比较实用,输入的格式问题不会吃掉配额。
## FAQ
**Q1:弹屏链路里查询超时该等多久?**
建议 1.5 秒以内的短超时。官方 OpenAPI 标注读取超时是 10 秒,走 HTTP 客户端直接调用时可以用 10 秒;弹屏这种用户可见的实时链路把超时压短,到点就降级。
**Q2:查询失败坐席界面上显示什么?**
什么都不显示,归属地位置留空。失败原因记到日志,不要推到坐席界面上。
**Q3:同一客户反复来电会重复调用接口吗?**
不会。缓存键是前 7 位号段,同一号段的号码复用同一条缓存记录。缓存过期时间可以设 30 天,数据每半年同步一次新增号段归属信息。
**Q4:为什么话术分流用 `type` 不用 `name`?**
`type` 是固定的 5 个数字枚举,取值稳定;`name` 实测会返回「移动全球通卡」「联通186卡」这类品牌名,形态不固定,用它做条件判断以后遇到新号段就得改代码。
**Q5:能拿到客户所在区县吗?**
不能。归属地返回的粒度是省和市,`city` 的文档定义就是「市」。更细的粒度需要其他数据源。
**Q6:归属地能当客户真实所在地用吗?**
不能。号码归属地反映的是号段归属,跟号码当前使用地不是一回事,号码可以异地使用。适合做初筛和展示,不适合作为地域判定的唯一依据。
## 下一步阅读
- [手机归属地查询 type 字段全解:1移动 2电信 3联通 4广电 -1未知怎么用](https://www.showapi.com/guides/phone-attribution-type-field-6) —— 话术分流的枚举依据
- [手机归属地查询按前 7 位号段做缓存:实测粒度与 Redis 实现](https://www.showapi.com/guides/phone-attribution-cache-6) —— 把重复来电的请求压下去
- [手机归属地查询返回 -2、-3、-4 是什么意思?错误码逐条对照与排查](https://www.showapi.com/guides/phone-attribution-error-codes-6) —— 降级分支要覆盖的状态码
- **本系列共 12 篇**:查看[手机归属地查询指南总目录](https://www.showapi.com/guides/phone-attribution-guides-6)





