手机归属地查询接进 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)
加载中...