很多系统判断手机号是否有效,逻辑其实很简单:
格式正确 → 发验证码 → 能收到 → 号码有效
刚注册时这样做没什么问题。
问题出在半年、一年甚至几年以后。
用户当年留下的手机号可能已经停机、销号,甚至被运营商重新投放给另一个人。此时这个号码看起来依然是一个正常的 11 位手机号,甚至重新恢复了“正常在网”,但号码背后的人已经变了。
这也是为什么存量手机号核验不能只看格式。
2026 年,二次放号问题仍然是通信和互联网账号管理中的现实问题。工信部公布的数据显示,“二次号码焕新”服务目前已经覆盖 249 款常用应用,服务用户超过 912 万人次,申请解绑应用超过 5.6 亿次。
对于自己维护会员、CRM、客户账号体系的平台来说,这背后其实还有另一个开发问题:
数据库里的手机号,现在到底处于什么状态?
一、先搞清楚:二次放号到底是什么?
举个很简单的例子。
用户 A 在 2022 年注册了你的平台:
用户ID:10086
手机号:18577881111
后来 A 不再使用这个号码,并办理了销号。
号码经过运营商回收后,未来可能重新投入市场,由用户 B 办理使用。
于是可能出现:
2022 年
18577881111 → 用户 A
2026 年
18577881111 → 用户 B
但你的数据库没有发生任何变化:
用户ID:10086
手机号:18577881111
这就麻烦了。
从数据库角度看,这个手机号“一直没变”。
从通信网络角度看,它也可能已经重新变成正常在网号码。
但从业务关系来看:
号码和原账号之间的对应关系已经可能失效。
工信系统对“二次号码”的解释也很明确,即运营商回收后重新启用的手机号码。二次放号后,新用户可能遇到号码仍绑定前任用户互联网账号的问题。
所以,“手机号当前在网”和“手机号仍属于原来的用户”,实际上是两件事。
二、这也是为什么只做正则校验远远不够
很多系统最基础的手机号判断仍然类似:
/^1[3-9]\\d{9}$/
它只能告诉你:
这个字符串长得像不像一个手机号。
它无法回答:
这个号码现在还在不在网?
有没有停机?
有没有销号?
是不是已经重新放号?
当前运营商是什么?
甚至:
18577881111
一个已经销号的号码和一个正常使用的号码,在数据库里长得一模一样。
所以如果业务里保存了大量历史手机号,仅做格式校验意义已经非常有限。
三、手机号在网状态 API 查的是什么?
这次接入的是探数API的一个手机号在网状态查询接口。
传入:
phone
即可查询手机号相关状态信息。
根据当前产品资料,该接口支持:
-
手机号在网状态查询
-
首次放号相关状态查询
-
二次放号相关状态查询
-
携号转网查询
-
运营商信息查询
-
手机号归属地查询
并支持全网手机号查询,不限地域。
这里要区分一个很重要的问题:
当前产品资料明确写明支持首次放号、二次放号以及携号转网相关查询,但目前提供给我的 JSON 示例中,并没有展示这些信息对应的具体字段。
所以本文不会自己创造类似:
is_second
release_type
port_status
这样的字段。
实际开发必须以接口真实返回结果为准。
当前已经能够从示例中确认的,是手机号在网状态、状态描述、运营商以及归属地信息。
四、实际请求并不复杂
接口地址:
https://market.aliyun.com/detail/cmapi00067375
请求方式:
GET
返回类型:
JSON
Query 参数目前只有一个:
| phone | string | 是 | 需要查询的手机号 |
例如:
phone=18577881111
使用 APPCODE 认证时,请求 Header 中需要携带:
Authorization:APPCODE 你的AppCode
完整请求可以理解为:
GET /phone_online?phone=18577881111
五、PHP 调用示例
产品资料已经提供了 PHP 示例,可以直接按照阿里云市场 APPCODE 的方式调用:
<?php
$host = "https://market.aliyun.com/detail/cmapi00067375";#接口地址
$path = "/phone_online";
$method = "GET";
$appcode = "你自己的AppCode";
$headers = array();
array_push(
$headers,
"Authorization:APPCODE " . $appcode
);
$querys = "phone=18577881111";
$bodys = "";
$url = $host . $path . "?" . $querys;
$curl = curl_init();
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_FAILONERROR, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, true);
if (1 == strpos("$".$host, "https://")) {
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
}
var_dump(curl_exec($curl));
?>
真正需要替换的主要有两个地方:
$appcode = "你自己的AppCode";
以及:
$querys = "phone=18577881111";
调用其他手机号时,把 phone 换成实际业务中的号码即可。
六、重点其实在返回结果,而不是请求代码
下面是当前接口资料提供的真实返回示例:
{
"code": 1,
"msg": "操作成功",
"data": {
"phone": "18577881111",
"isp": "联通",
"desc": "正常",
"status": 0,
"province": "广西",
"city": "南宁市",
"zipcode": "530000"
}
}
相比调用代码,我更建议开发时重点关注 status 和 desc。
七、status 和 desc 应该怎么看?
status:判断是否在网
当前资料给出的定义是:
| 0 | 在网 |
| 1 | 不在网 |
示例返回:
"status": 0
意味着这个手机号目前处于在网状态。
desc:看具体状态描述
示例中:
"desc": "正常"
表示当前状态正常。
产品资料中还明确给出了几种不在网情况下可能出现的描述:
停机
在网不可用
销号
所以实际业务中不建议只保存:
有效 / 无效
至少应该保留接口原始状态。
例如:
status = 0
desc = 正常
和:
status = 1
desc = 销号
虽然最终都可以转成业务系统自己的判断逻辑,但两者背后的意义完全不同。
八、为什么“销号”和“二次放号”一定要分开理解?
这里是这类接口最容易被忽略的地方。
假设一个号码经历:
正常使用
↓
销号
↓
运营商回收
↓
重新放号
↓
新用户正常使用
如果只在“销号”阶段查询:
status = 1
很好理解,这个号码暂时不在网。
但如果等它重新放号以后再查询:
status = 0
desc = 正常
此时号码又重新“正常”了。
问题是:
正常使用它的人已经可能换了。
因此对于一些存量账号管理场景:
status = 0
并不能直接等价成:
这个手机号仍然属于历史账号原来的用户
这也是“在网状态查询”和“二次放号核查”之间最大的区别。
2026 年 7 月公开报道仍在讨论二次放号导致的新旧用户账号绑定、注册受阻以及信息关联问题,也说明手机号生命周期变化已经不只是通信层的问题,而会直接传导到互联网账号体系。
九、放到业务系统里,应该怎么用?
如果让我设计一个存量手机号检查流程,我不会在每个页面打开时都去查一次。
更合理的是把它放进特定业务节点。
例如数据库中有一批很久没有更新的客户资料:
用户A 138******** 2022年最后更新
用户B 186******** 2023年最后更新
用户C 159******** 2026年刚更新
可以优先核查长期没有更新的手机号。
简单的处理逻辑类似:
历史手机号
│
▼
调用手机号状态查询
│
├── 正常在网
│ ↓
│ 保留当前状态
│
└── 不在网
↓
查看 desc
│
├── 停机
├── 在网不可用
└── 销号
如果业务还需要进一步判断号码生命周期,则再结合接口提供的首次放号、二次放号等相关信息进行处理。
这比直接把所有号码粗暴分成:
能用
不能用
要有意义得多。
十、一个比较实际的数据库设计思路
很多项目接入第三方状态接口之后,会直接写:
phone_valid = 1
我反而不太建议这么做。
因为这会把大量有价值的信息丢掉。
例如可以根据自己业务需要保存:
phone
status
desc
isp
province
city
check_time
查询结果:
18577881111
0
正常
联通
广西
南宁市
2026-09-06
这样以后你至少知道:
这个号码是在什么时间查过,当时是什么状态。
而不是半年以后看到一个:
phone_valid = 1
却完全不知道这个“1”是什么时候得到的。
这里的 check_time 属于自己业务系统记录的查询时间,并不是当前接口返回字段。
十一、携号转网还会带来另一个容易踩的坑
以前很多系统喜欢通过手机号号段判断运营商。
但携号转网存在以后:
原始号段
和:
当前实际运营商
就不能简单画等号了。
因此,如果业务真的需要知道号码当前运营商,更合理的方式是使用实际查询结果。
当前示例返回:
"isp": "联通"
这个字段就是运营商信息。
产品资料同时明确说明支持携号转网查询。
不过同样需要强调:当前给出的 JSON 示例没有展示携号转网对应的独立字段,因此不能自行假设字段结构。
十二、这个接口和普通“空号检测”有什么区别?
乍一看,两者好像都在查手机号。
但实际解决的问题并不完全一样。
空号检测更常见的需求是:
这一批号码还能不能触达?
例如短信群发之前先过滤空号。
而手机号在网状态 + 二次放号核查更偏向:
我系统里保存的这个历史号码,现在到底是什么状态?
甚至进一步追问:
它现在虽然正常,但还是不是原来的号码关系?
所以在会员系统、CRM、长期账号体系里,后一个问题往往更值得关注。
写在最后
手机号是很多互联网系统里保存时间最长的一类用户数据。
问题在于:
手机号这个字符串不会自己告诉你,它背后的状态已经发生了变化。
138********
去年可能正常。
今年可能停机。
之后可能销号。
再过一段时间,又可能重新被别人使用。
数据库看到的,却始终只是同样的 11 位数字。
这也是手机号在网状态查询真正有价值的地方。
它不是用来判断:
“这是不是一个手机号?”
而是进一步回答:
“这个手机号现在到底是什么状态?”
如果业务涉及长期会员账号、历史客户资料或号码生命周期核查,仅靠正则和验证码已经覆盖不了全部问题。把在网状态、状态描述以及二次放号等信息纳入手机号数据维护流程,会比单纯保存一个号码本身更可靠。




