手机归属地查询:在注册表单里做实时校验与运营商识别

约 14 分钟 进阶手机号码归属地查询表单校验注册防错运营商识别
# 手机归属地查询:在注册表单里做实时校验与运营商识别 接口:手机归属地查询(apiCode=6,接入点 6-1)|是否免费:是,注册后默认可调用,设使用档次限制|请求方式:POST / GET|返回格式:JSON|适用人群:做注册、开户、留资表单的前后端开发者|阅读时间:约 8 分钟|最后实测核对:2026-09-18 注册表单里的手机号输入框,除了「是不是 11 位数字」,还能顺手做两件事:把归属地显示在输入框下方,让用户一眼确认没输错;把号码归属地与用户选的省份做一次同源比对,不一致时给个提示。这两件事都不需要额外字段,一次查询就够了。 ## 核心要点 - 前端先在本地拦格式,只把干净的号码发给后端,避免无效调用占用额度。 - 服务端拿到的号码按前 7 位号段查询即可,实测后 4 位不影响归属地结果。 - 地域一致性比对用 `provCode`,它和身份证前 6 位同源,比用省名文字做比对准。 ## 校验要分两层做 输入框里的号码要经过两次检查,职责不同。 前端这一层管即时反馈。用户在输入过程中最容易犯的错是数字位不够、混进字母、全角输入法打出全角数字。这些都能在本地正则里判掉,不用等到提交。 服务端这一层管真实性。号码格式对了不代表号段存在,`00000000000` 就是 11 位纯数字,但接口会返回 `-5`。服务端查一次,才知道这个号段在不在库里。 两层都用同一个判据:先清理输入里的空格和连字符,再取前 7 位号段。接口实测就是这个口径,`138-0013-8000` 和 `13800138000` 返回完全一样。 ## 前端:本地校验加防抖 ```javascript const PHONE_SEGMENT_LEN = 7; /** 清理并取前 7 位号段;不合规返回 null */ function toSegment(raw) { const cleaned = String(raw ?? "").trim().replace(/[-\s]/g, ""); if (!/^\d{7,}$/.test(cleaned)) return null; // 全角、字母、+、. 都在这里被挡下 if (cleaned[0] === "0") return null; // 首位为 0 的号段不存在 return cleaned.slice(0, PHONE_SEGMENT_LEN); } let timer = null; const cache = new Map(); // 同一会话内同一号段不重复查 inputEl.addEventListener("input", (e) => { clearTimeout(timer); const seg = toSegment(e.target.value); hintEl.textContent = ""; if (!seg) return; // 本地就没过,不用发请求 if (cache.has(seg)) { // 命中本地缓存,直接展示 render(cache.get(seg)); return; } timer = setTimeout(() => askServer(seg), 400); // 防抖 400ms,输入停下才查 }); async function askServer(seg) { const resp = await fetch(`/api/phone-locate?seg=${seg}`); const body = await resp.json(); if (body.ok) { cache.set(seg, body); render(body); } else { hintEl.textContent = `该号段查不到归属地(ret_code=${body.ret_code})`; } } ``` ## 服务端:查询与地域比对 ```python import requests from flask import Flask, request, jsonify app = Flask(__name__) API_URL = "https://route.showapi.com/6-1" APP_KEY = "YOUR_APPKEY" # 正式环境从配置或密钥管理服务读取 def locate(seg: str) -> dict: resp = requests.get( API_URL, params={"appKey": APP_KEY, "num": seg}, timeout=10, # 对齐官方 x-read-timeout ) data = resp.json() if data.get("showapi_res_code") != 0: return {"ok": False, "ret_code": data.get("showapi_res_code"), "remark": data.get("showapi_res_error")} body = data.get("showapi_res_body", {}) if body.get("ret_code") != 0: # 失败不扣点数,可以直接把 ret_code 返给前端做提示 return {"ok": False, "ret_code": body.get("ret_code"), "remark": body.get("remark")} return { "ok": True, "prov": body.get("prov"), "city": body.get("city"), "carrier": body.get("name"), "carrier_type": body.get("type"), "prov_code": body.get("provCode"), "seg": body.get("num"), } @app.post("/api/phone-locate") def phone_locate(): seg = (request.json or {}).get("seg", "") if not seg.isdigit() or len(seg) != 7 or seg[0] == "0": return jsonify({"ok": False, "ret_code": -2, "remark": "号段参数不合规"}), 400 return jsonify(locate(seg)) @app.post("/api/register") def register(): payload = request.json or {} seg = (payload.get("seg") or "").strip() declared_prov_code = payload.get("prov_code") # 用户在表单里选的省份编码 result = locate(seg) if seg else {"ok": False, "ret_code": -2} if not result["ok"]: # 校验失败不阻断注册主流程,只标记,避免误伤 return jsonify({"created": True, "location": None, "mismatch": None}) mismatch = None if declared_prov_code and result["prov_code"]: mismatch = str(declared_prov_code)[:2] != str(result["prov_code"])[:2] return jsonify({"created": True, "location": result, "mismatch": mismatch}) ``` `provCode` 的取值口径是「本省身份证的前几位编码」,实测 `13800138000` 落在北京时返回 `110000`。拿它和用户选的省份编码前两位比对,就能判断号码归属地和申报地域是否同源。省名文字不适合做这个比对,「北京」和「北京市」这类写法差异会让比对结果失真。 ## 提示文案分档处理 `ret_code` 的不同取值对应不同的用户提示,分开处理能减少客诉。 | `ret_code` | 用户提示 | 是否记入风险标记 | |------------|----------|------------------| | 0 | 显示「省份 · 城市 · 运营商」 | 否 | | -2 / -3 / -4 | 「手机号格式有误,请检查」 | 否,属于输入问题 | | -1 | 「该号段暂无归属地信息」 | 是,可标记待人工核验 | | -5 / -6 | 「该号段查不到归属地」 | 是,可标记待人工核验 | -2 到 -4 是输入侧的问题,提示用户改就行。`-1`、`-5`、`-6` 说明号码本身可疑,适合进风险标记流程。这条错误码的完整触发条件在[错误码逐条对照](https://www.showapi.com/guides/phone-attribution-error-codes-6)。 ## 表单场景的调用量控制 表单页面的查询请求比后端批量任务更零散,用户每改一次号码就可能触发一次。做法有三条。 第一,前端防抖 400 毫秒,只在输入停顿后发请求。第二,同一浏览器会话里对同一号段做本地缓存,实测后 4 位不影响结果,缓存键用前 7 位即可。第三,服务端对同一号段也加一层短期缓存,多实例部署时用 Redis,键就是前 7 位号段。 免费档位从 L0 起算,每日调用量与并发上限见[免费接口与档位说明](https://www.showapi.com/free-api)。缓存策略的完整实现另有一篇:[手机归属地查询按前 7 位号段做缓存](https://www.showapi.com/guides/phone-attribution-cache-6)。 ## FAQ **Q1:前端一定要调接口吗?** 不必。前端本地正则能挡掉大部分格式错误,接口只在号码格式合规后调用一次,用于确认号段存在并取回归属地。这样能显著减少请求量。 **Q2:缓存键为什么用前 7 位?** 实测确认归属地结果只跟前 7 位号段有关。`13800130000` 和 `13800139999` 的返回完全一致,后 4 位不参与匹配。用前 7 位做键,命中率更高。 **Q3:地域比对为什么不直接比省名?** 省名有多种写法,「北京」和「北京市」都会被用户选中。`provCode` 是数字编码,实测北京返回 `110000`,做前缀比对不会因为写法和表述差异出错。 **Q4:查询失败要阻断用户注册吗?** 不建议阻断。`-2` 到 `-4` 是输入问题,提示用户修改即可;`-1`、`-5`、`-6` 可以把号码标记为待核验,主流程照常放行,避免把真实用户挡在门外。 **Q5:调用失败会扣次数吗?** 不会。失败时 `showapi_fee_num` 为 0,文档口径是失败不扣点数。 **Q6:一次表单要查几次接口?** 建议只查一次。拿到号段后本地缓存,同一页面内不再重复请求。后端注册时若需要归属地,从缓存或已有记录里取,不重复调用。 ## 下一步阅读 - [手机归属地查询返回 -2、-3、-4 是什么意思?错误码逐条对照与排查](https://www.showapi.com/guides/phone-attribution-error-codes-6) —— 提示文案分档的依据 - [手机归属地查询按前 7 位号段做缓存:实测粒度与 Redis 实现](https://www.showapi.com/guides/phone-attribution-cache-6) —— 把表单请求量压下来 - [手机归属地查询 type 字段全解:1移动 2电信 3联通 4广电 -1未知怎么用](https://www.showapi.com/guides/phone-attribution-type-field-6) —— 运营商分支怎么写 - [手机归属地查询接进 CRM:来电弹屏显示归属地与运营商](https://www.showapi.com/guides/phone-attribution-crm-6) —— 同一套查询封装用到客服侧 - **本系列共 12 篇**:查看[手机归属地查询指南总目录](https://www.showapi.com/guides/phone-attribution-guides-6)
加载中...