手机归属地查询:在注册表单里做实时校验与运营商识别
约 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)





