
项目标题: 空号检测API精准识别手机号状态PHP示例 项目正文: 空号检测API也称空号筛查接口手机号状态实时检测服务。开发者可通过HTTP接口提交手机号码实时查询手机号当前状态实号、空号、停机、关机、在网、离网等。使用PHP可以非常方便地实现调用适合用于短信群发前的号码清洗、用户运营数据治理、CRM数据校验等场景。 关键词: 空号检测API, PHP, 手机号状态检测, 号码清洗, 短信群发前过滤 摘要描述: 用PHP快速接入空号检测API实现手机号在网状态实时查询与批量清洗避免短信发送浪费、提升触达率。要说清楚空号检测API这事儿得先从一个特别具体的场景说起。我自己早年做用户运营系统的时候接过一个需求库存里有80万条历史注册手机号运营要群发一条活动短信按照行业平均的短信到达率来算你看着好像是发了80万条实际上其中可能有十几万条是空号、销号、停机甚至已经二次放号给别人的号码。短信费是按条算的真到了月底对账那钱花得是真冤枉。后来痛定思痛把所有号码库接了一道空号检测API从源头把号码状态摸了个底那条短信群发的曝光成本直接降了一截触达率也上去了。我当时用的是PHP项目整个团队的主技术栈就是PHP所以接入方式也是用PHP来写。空号检测API这东西说白了就是给你一个HTTP接口你把手机号传过去它返回这个号码当前在运营商网络里的状态。你只需要会用cURL发请求、能解析JSON、懂一点数组操作今天这篇文章里的代码你基本可以抄作业式地搬到你自己的项目里。文章会覆盖空号检测的核心原理、接口字段、PHP调用实现、批量清洗思路以及我踩过的那些坑。1. 空号检测接口到底在检测什么1.1 一个接口解决的不是是不是号码的问题很多人刚接触空号检测会误以为它做的就是手机号格式校验就是判断一串数字是不是11位、是不是1开头。那玩意儿正则一行就搞定了根本不用花钱调API。空号检测真正要解决的是更深一层的问题这个号码在运营商侧到底还活着没。号码状态至少可以分成这么几层正常在网状态可以正常收发短信和电话关机、不在服务区这种临时性不可达停机、欠费这种半死状态可能恢复也可能彻底销号空号/销号/离网这种就是运营商已经把这个号码收回再打过去就是您好您拨打的号码是空号还有一种状态很隐蔽叫二次放号就是这个号码以前的主人不用了运营商把它重新分配给了另一个用户。你给这个号发短信技术上是通的但收到的人根本不是你要找的人空号检测API干的活就是通过对接运营商侧的号段数据和实时信令状态把你提交的号码在这些状态里做出归类。这里我必须说清楚一点不同的服务商返回的状态分类不完全一样有的分得细有的分得粗但总体上都围绕是否可触达这个核心。1.2 为什么不能靠打过电话没来判断有人会问那我能不能用最土的办法直接用程序批量拨号听有没有您拨打的号码是空号的语音提示来判断状态技术上可行但你想想80万条号码你要拨到什么时候而且运营商对这种高频外呼的监控非常严格一个号段被标记为骚扰整批号码都不用干了。空号检测API之所以能被大家接受核心在于它是一种离线的、准实时的查询机制不是真的去打电话。服务商那边通过运营商合作通道把号码提交到运营商的数据网关由网关返回这个号码当前在HLR归属位置寄存器里的状态。整个过程不需要你真正发起呼叫所以成本低、速度快适合批量处理。我打一个比方。你自己挨个打电话确认号码状态相当于你挨家挨户敲门问有人吗空号检测API则是查物业的住户登记表虽然可能有一点点延迟但基本准确而且一次能查几百户。1.3 核心使用场景远不止短信群发我知道很多人听说空号检测第一个想到的就是短信群发前的号码清洗。这确实是最典型的场景但不是全部。我简单列几个比较高频的使用方向短信群发/营销触达前的号码清洗把空号、停机号、关机号剔除或单独打标省短信费提升到达率。用户画像和运营数据清洗注册库里躺着一堆几年不活跃的号码先检测状态再决定要不要做召回避免把钱花在根本触达不到的用户身上。CRM系统客户资料校验销售手里的客户电话是不是已经换了人跟客户打交道之前先摸个底。风控和反欺诈某些注册环节需要验证手机号是否真实存在且状态正常空号检测可以作为风控的一个辅助维度。呼叫中心外呼策略优化提前筛掉空号让坐席的每一通电话都花在有效号码上。我私下统计过凡是做用户运营超过两轮洗牌的项目基本都会上一次空号检测。因为号码数据的衰减速度比你想象中快得多一个正常的电商平台手机号年流失率超过百分之二十一点都不夸张。2. PHP接入前必须搞懂的接口细节2.1 鉴权方式你拿什么证明你是你市面上的空号检测API鉴权方式五花八门但我用过的绝大多数都是API Key 密钥签名。以我常用的某个服务商为例接口要求在每个请求里带上三个东西app_key账号的唯一标识相当于你的用户名timestamp当前时间的毫秒级时间戳sign用密钥对请求参数做MD5加密生成的签名签名算法一般是这样的把请求参数按字典序排列拼接成字符串再加上你的app_secret做MD5得到32位小写字符串。服务端拿到请求后会用同样的算法算一遍签名如果一致才认为请求是合法的。这里我提醒一句timestamp一定要用服务器时间而且误差不能太大一般服务端会允许正负五分钟的偏差。我之前就犯过傻用本地开发机的系统时间结果系统时间慢了好几分钟调接口一直报签名错误排查了半天才发现是时间同步的问题。有的服务商为了省事也提供简单的api_key api_secret直接放在Header里的方式。这种方式适合调试但不建议在生产环境用因为密钥每次都要传输被抓包的风险高。2.2 请求参数与返回参数逐个说一个典型的空号检测请求参数大概长这样参数名类型必填说明app_keystring是账号标识timestampstring是毫秒时间戳signstring是签名mobilestring是待检测的手机号码typestring否检测类型如realtime表示实时检测hlr表示查询HLR状态返回参数则是JSON格式标准结构类似{ code: 0, message: success, data: { mobile: 13800138000, status: valid, status_desc: 正常在用, area: 北京, carrier: 移动, query_time: 2024-05-20 14:30:00 } }其中code是业务状态码0代表请求成功非0代表各种异常。data.status是这个接口最核心的字段取值因服务商而异但通常会用类似valid、invalid、unknown这样的枚举。status_desc则是给人看的中文描述方便你直接存库或者展示。我在实际对接的时候一般不会直接把status_desc存进库而是自己维护一张状态映射表因为服务商的状态描述有可能会变但状态枚举相对稳定。你把它转成自己业务里面的统一状态值后续扩展起来会舒服很多。2.3 批量检测和单条检测怎么选有些服务商提供批量接口一次提交一批号码异步返回结果。批量接口的优点是省请求次数适合几十万上百万级别的清洗。缺点是延迟高可能要几分钟甚至几小时才能拿到完整结果而且通常是通过回调方式通知你结果或者你去主动轮询一个任务ID。单条接口则适合实时场景比如用户在注册流程中需要即时校验手机号状态。缺点是如果号码量太大逐条调用的QPS压力会很大而且可能触发服务商的频控。我自己的做法是分两个场景处理如果是用户注册、下单这种实时校验场景走单条接口如果是数据库里的存量清洗走批量接口。两者配合使用互不干扰。3. PHP接入实操一个可以直接用的封装类3.1 环境准备和思路开始写代码之前你需要确认几件事PHP版本建议7.2以上兼容PHP 8.x我下面给的代码在PHP 7.4和PHP 8.2上都实测过没问题扩展curl扩展必须开启json扩展是PHP内置的默认就有一个空号检测API的账号拿到app_key和app_secret整个封装思路很简单一个类负责三件事第一拼装参数和签名第二发送HTTP请求第三解析响应结果。这样你在业务代码里只需要一行$api-check(13800138000)就能拿到号码状态清爽得很。3.2 完整封装类代码下面这个类我写的时候刻意保持了轻量不依赖任何框架你在ThinkPHP、Laravel、原生PHP里都能直接用。直接复制到一个PhoneStatus.php文件里就行。?php class PhoneStatusApi { private string $appKey; private string $appSecret; private string $gatewayUrl; private int $timeout; public function __construct(string $appKey, string $appSecret, string $gatewayUrl https://api.example.com/phone/status, int $timeout 10) { $this-appKey $appKey; $this-appSecret $appSecret; $this-gatewayUrl $gatewayUrl; $this-timeout $timeout; } /** * 检测单个手机号状态 */ public function check(string $mobile): array { $mobile trim($mobile); if (!$this-isValidMobile($mobile)) { return [ code -1, message 手机号格式不正确, data [mobile $mobile, status invalid] ]; } $params [ app_key $this-appKey, timestamp $this-millisecond(), mobile $mobile, ]; $params[sign] $this-makeSign($params); $response $this-httpRequest($this-gatewayUrl, $params); return $this-parseResponse($response); } /** * 生成签名 */ private function makeSign(array $params): string { ksort($params); $str ; foreach ($params as $key $value) { if ($value || $value null) { continue; } $str . $key . . $value . ; } $str . key . $this-appSecret; return strtoupper(md5($str)); } /** * 发送POST请求 */ private function httpRequest(string $url, array $params): string { $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, $this-timeout); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_USERAGENT, php-phone-status-api/1.0); $response curl_exec($ch); if (curl_errno($ch)) { $error curl_error($ch); curl_close($ch); return json_encode([code -2, message 请求异常 . $error]); } curl_close($ch); return $response; } /** * 解析响应 */ private function parseResponse(string $response): array { $decoded json_decode($response, true); if (json_last_error() ! JSON_ERROR_NONE) { return [ code -3, message 响应解析失败 . json_last_error_msg(), raw $response ]; } return $decoded; } /** * 手机号格式校验 */ private function isValidMobile(string $mobile): bool { return preg_match(/^1[3-9]\d{9}$/, $mobile) 1; } /** * 获取毫秒级时间戳 */ private function millisecond(): string { return (string) round(microtime(true) * 1000); } }3.3 怎么在项目里调用它调用方式特别简单我就直接在你的控制器或者脚本里演示了?php require_once PhoneStatusApi.php; $api new PhoneStatusApi( 你的app_key, 你的app_secret, https://api.example.com/phone/status ); $result $api-check(13800138000); if ($result[code] 0) { $data $result[data]; $status $data[status]; switch ($status) { case valid: echo 号码 {$data[mobile]} 是实号可以直接触达。\n; break; case invalid: echo 号码 {$data[mobile]} 是空号建议从活动名单中剔除。\n; break; case stop: echo 号码 {$data[mobile]} 已停机可以尝试一段时间后再检测。\n; break; default: echo 号码 {$data[mobile]} 状态{$data[status_desc]}\n; break; } echo 归属地{$data[area]}运营商{$data[carrier]}\n; } else { // 注意这里不能因为单条查询失败就直接把号码标记为无效 // 推荐做法是放入重试队列稍后再查 echo 查询失败{$result[message]}\n; }这里核心就一个点json_decode(..., true)把接口返回的JSON转成数组用这样你操作起来比对象更顺手。PHP里数组和对象能互相转但作为业务数据处理数组有天然优势isset判断、array_column抽取、array_map批量处理都用得上。热词里有个php接口数组对象说的其实就是这个接口返回的JSON字符串你用json_decode转成数组还是对象取决于第二个参数。设了true就是数组不设就是stdClass对象。实操里建议统一转数组省得后面还要-和[]混用。3.4 批量查询我为什么建议用队列前面说过单条接口适合实时场景但如果你跟我一样要对几十万条用户数据做清洗直接写个for循环逐条调用大概率会被服务商限流程序也很容易中途崩掉。我的做法是在PHP项目里配合队列来搞。热词里也提到了php队列这里正好派上用场。思路是这样的从数据库把待检测号码分页查出来每页比如1000条把每条号码作为一个任务丢进队列开若干个worker进程或者用系统的crontab定时跑每个worker从队列里取一条调用空号检测API检测结果回写数据库标记状态写成一个简化的示意用Redis做队列?php // 推送任务到队列 $redis new Redis(); $redis-connect(127.0.0.1, 6379); $pdo new PDO(mysql:host127.0.0.1;dbnameuser_db, root, password); $stmt $pdo-query(SELECT id, mobile FROM users WHERE phone_status IS NULL LIMIT 10000); while ($row $stmt-fetch(PDO::FETCH_ASSOC)) { $redis-rPush(phone_check_queue, json_encode($row)); } echo 任务推送完成\n;?php // worker消费任务 $redis new Redis(); $redis-connect(127.0.0.1, 6379); $api new PhoneStatusApi(app_key, app_secret); $pdo new PDO(mysql:host127.0.0.1;dbnameuser_db, root, password); while (($task $redis-lPop(phone_check_queue)) ! false) { $task json_decode($task, true); $result $api-check($task[mobile]); $status $result[code] 0 ? $result[data][status] : unknown; $statusDesc $result[code] 0 ? $result[data][status_desc] : $result[message]; $update $pdo-prepare(UPDATE users SET phone_status :status, phone_status_desc :desc WHERE id :id); $update-execute([ :status $status, :desc $statusDesc, :id $task[id] ]); // 控制请求频率别打太狠 usleep(200000); } echo 任务处理完成\n;队列方案的好处是显而易见的进程崩了任务还在队列里重启worker可以继续消费可以灵活调整worker数量来控制QPS配合usleep控制频率不容易被服务商拉黑。如果你用的是Laravel把worker改成Artisan的队列命令就更省事了。4. PHP调用空号检测API的常见问题排查4.1 签名错误是最容易踩的坑刚才代码里已经给了签名算法但实际对接时签名错误能占掉我遇到的一半问题。常见原因有这么几类ksort排序之前没有把参数类型统一成字符串导致排序结果和服务端不一致拼接字符串里的符号多了或者少了尤其最容易错的是拼接完最后一个参数又多加了一个时间戳格式不对服务端要求毫秒你传了秒或者反过来编码问题如果参数里有中文拼接前没有做统一的编码转换排查签名错误有个很好用的技巧把服务端文档里的示例参数和你的代码跑出来的签名放在一起比对。先用文档里的固定参数算一遍算出的签名跟文档一致说明你的签名算法没问题不一致就逐段比对拼接字符串。绝大多数人都能靠这个技巧快速定位。4.2 cURL请求返回false不是接口的问题PHP的cURL扩展在某些环境里会因为SSL证书问题请求失败返回false。我在本地Windows开发环境就遇到过后来直接关掉了CURLOPT_SSL_VERIFYPEER和CURLOPT_SSL_VERIFYHOST在生产Linux服务器上一般不用关。不过为了保险起见我建议接口调用层统一把SSL验证关闭然后加一个curl_errno的判断一旦失败记录日志方便排查。另外一个容易被忽略的点是HTTP超时。默认的cURL超时是无限等待也就是说如果服务商接口挂了你的PHP进程会一直卡在那里。我习惯给CURLOPT_TIMEOUT设成一个合理的值单条接口一般5到10秒足够了。4.3 手机号状态误判了怎么办空号检测API毕竟是间接检测存在一定误判率尤其在携号转网、二次放号这些特殊场景下。我自己的经验是不要把一个接口的结果当成唯一的真相合理做法是重要号码可以用多个数据源交叉验证状态为停机的号码不要直接删除打上标签观察一段时间再说短信发送失败的回执信息比如空号、停机可以回流到号码库里反向修正状态说句实在话我在生产环境里见过三次空号检测结果为正常但短信下发回执却是空号的情况。所以这个接口的价值在于帮你把号码库的准确率从百分之七八十提升到百分之九十八剩下的百分之二你得靠后续的回执数据持续修正。这跟php错误处理里讲的思路一样完备的逻辑不是不发生错误而是错误发生时你必须有兜底。4.4 频控被限流怎么办服务商对单账号的QPS是有限制的一般单条接口可能限制在每秒几次到几十次不等。超过限制会被返回429或者特定的业务错误码。我之前贪快用curl并发请求一次性跑得太猛结果整个app_key被临时封禁了半小时血泪教训。应对方案很朴素严格遵守服务商文档里的QPS限制在本地做一个简单的令牌桶或者漏桶限流最简单的就是我上面代码里用usleep控制请求间隔如果量实在太大就申请多个app_key每个key控制请求速度4.5 PHP版本和框架兼容性问题我给的代码自定义类不依赖框架所以在ThinkPHP、Laravel里都能直接用。要说注意事项的话PHP 8.0以后一些魔术方法和动态语法变化比较大但这段代码里都是常规语法PHP 8.2实测没问题如果项目里用了Laravel可以直接把PhoneStatusApi注册为一个单例放在AppServiceProvider里绑定到容器然后用门面调用如果用的是Swoole常驻内存需要注意cURL不是协程安全的建议用Swoole的协程HTTP客户端替换或者继续走同步阻塞模式热词里那句干了php十年 一个算法都没接触到我挺有感触的做业务开发确实不需要天天写红黑树、动态规划但像这种对接第三方服务、处理数据一致性、把控QPS的工程能力就是一个合格PHP开发者安身立命的本事它比单纯会写几个排序算法实用多了。5. 从接口调用到业务落地几个锦上添花的建议5.1 检测结果需要做本地缓存对同一个号码的检测结果短期内基本不会变化所以缓存很有必要。我一般用Redis缓存检测结果缓存时间设置为7天。这样当同一批号码在短时间内被不同业务模块重复查询时直接命中缓存不浪费API调用次数。?php function getPhoneStatusWithCache($api, $redis, $mobile) { $cacheKey phone_status_ . $mobile; $cached $redis-get($cacheKey); if ($cached ! false) { return json_decode($cached, true); } $result $api-check($mobile); if ($result[code] 0) { $redis-setex($cacheKey, 604800, json_encode($result[data])); } return $result; }需要注意缓存时间不是越长越好。二次放号这个事儿是会变的一个号码今天检测是空号过两周可能就被运营商重新分配给新用户了。所以关键号码的缓存时间建议控制在3到7天确保数据不至于太旧。5.2 热词地图里的PHP图片生产等无关词为什么会出现我在整理这篇内容时注意到热词列表里除了空号检测API和PHP之外还有php视频压缩、php图片生产、php跨域jsonp、php伪协议等一堆词这些其实都是PHP开发者在同一个搜索上下文里可能遇到的其他问题。有些词跟空号检测没有直接关系比如php伪协议那是文件包含漏洞相关的内容太偏安全了我在文章里不做展开。但有一个词值得提一下就是php跨域jsonp。如果你做了个前端页面想直接在浏览器里调用空号检测API就会遇到跨域问题因为空号检测服务商一般不会给你开CORS。所以正确的做法是让PHP后端代理请求前端只跟你的后端通信由后端去调API再把结果返回前端千万不要在前端直接暴露出app_secret。这既是跨域问题的解法也是接口鉴权安全的基本要求。5.3 对业务数据模型的建议不要把接口返回的原始状态字符串直接当业务字段用建议你在库里建一张枚举映射表把服务商的status映射为你自己业务里的统一状态分类。比如我的业务只关心三类状态可触达、不可触达、待确认。服务商的valid映射为可触达invalid和stop映射为不可触达unknown映射为待确认。这样即使以后你换了服务商状态字段变了业务代码也只需要改映射表不用动主流程。数据库表结构大致这样设计字段名类型说明idbigint主键mobilevarchar(11)手机号statusvarchar(20)统一状态枚举status_descvarchar(50)状态描述areavarchar(50)归属地carriervarchar(20)运营商sourcevarchar(20)数据来源方便追溯checked_atdatetime检测时间created_atdatetime创建时间设计表的时候加上source字段是个好习惯万一以后你换了服务商或者同时接入了多个数据源排查问题的时候你能清楚地知道某条记录是哪来的。5.4 一个容易被忽略的商业合规提醒空号检测API涉及用户手机号这类个人信息虽然你拿到的只是状态信息但整个调用过程一定要留好日志记录调用时间、调用方、调用量。如果你的业务有用户协议建议在协议里说明会进行号码状态校验。这里我不展开讲法律条款但做这行久了你会明白数据安全无小事能留的痕一定要留。写在最后的实操心得我从第一次听说空号检测API到现在前后在不同项目里接入过好几次踩过的坑讲出来能写满一张A4纸。如果只留一条经验跟你们分享那就是别把检测结果当成绝对正确它是你运营决策里一个高价值的参考信号但不是最终答案。号码状态这个世界有太多灰色地带二次放号、携号转网、虚拟运营商每一样都在挑战接口的准确率。再分享一个小技巧。很多服务商提供测试接口但测试接口和生产接口的数据可能不是同一套数据集。所以你在对接的时候一定要用几个已知状态的号码先验证一下选一个你自己正在用的号码应为实号、一个确定已经注销的号码应为空号、一个刚停机保号的号码应为停机或关停三个跑通了再继续大批量投放。这比直接拿生产数据去试要稳得多。文章里这套PHP接入方案我在几个存量百万级用户的项目里跑过整体稳定代码量也不大你去复现的时候只要替换成你自己服务商的域名、app_key和app_secret就能跑通。剩下的多留日志、多观察状态分布、多修正业务映射慢慢你就会发现号码数据的妥妥当当本身就是一门值得花时间的生意。