Skip to content

医院在线挂号怎么防黄牛?PHP 用 IP 归属地给号源加一道闸 ​

专家号放出 30 秒被抢空,患者一台手机怎么也挂不上,黄牛的抢号软件 0.02 秒就能锁号——这是各地媒体反复报道过的真实场景。号源风控和电商秒杀还不一样:号源不能加库存,放号时刻一过就没了,而且抢号失败的往往是真正需要就诊的人。技术侧能做的,是在放号瞬间尽量区分「真人」和「脚本」。IP 归属地是成本最低的第一道闸,这篇用 PHP 讲怎么落地。

一、黄牛抢号靠什么 ​

抢号软件的核心就三样:高速请求、批量账号、干净的出口 IP。

  • 高速请求:在放号前 1 秒并发打挂号接口,靠手速拼不过;
  • 批量账号:用接码平台注册一堆账号,一个号抢到就卖号或改绑;
  • 干净的出口 IP:这是最容易忽略的一环。医院风控一旦发现「一个 IP 疯狂抢号」,立刻会拉黑,所以黄牛都挂代理池(住宅代理、机房代理轮流换 IP)来绕过。反过来想:谁能识别出「这个 IP 是机房或代理出口,不是正常患者的家庭宽带」,谁就把黄牛挡在了第一道门外。

想明白这一点,方案就清楚了:不用追求识别每个黄牛,先把最高危的流量(机房 IP、异地高频 IP)拦下来,剩下的交给验证码和多因子。

二、方案设计 ​

放号接口前加一道「IP 预检」,分三档处理:

档位判定条件处理
高危ip_type 为 IDC 机房 / 企业出口;或命中同 IP 高频抢号直接拒绝或强制滑块验证
可疑异地 IP(与就诊城市/账号注册地明显不符)且新号弹验证码,放行但打标观察
正常家庭宽带(ISP 为电信/联通/移动),频率正常正常放行

两个要点:IP 判断必须放抢号动作之前(预检),不要在抢到号之后再补查,那时候号已经被占了;频率计数用 Redis 的 INCR + EXPIRE,秒级过期,别自己写数组存内存,多实例下会漏。

三、PHP 实现 ​

下面这段 PHP 可以直接跑:接收客户端 IP,调 IP9 免费接口拿归属地信息,再按「机房/企业 + 高频」两个信号打分,输出放行/验证码/拒绝三种建议。

php
<?php
// 挂号 IP 预检:PHP + IP9 归属地接口
// 免费版 GET https://ip9.com.cn/get?ip=<ip>,60 次/分钟/IP 够挂号场景用

function ip_geo($ip) {
    // 这里换成你们自己的出口(后端服务端调用,不要用用户侧代理)
    $url = "https://ip9.com.cn/get?ip=" . urlencode($ip);
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 3,
        CURLOPT_USERAGENT      => "hospital-reg-guard/1.0",
    ]);
    $body = curl_exec($ch);
    curl_close($ch);
    $data = json_decode($body, true);
    if (($data['ret'] ?? 0) !== 200) {
        throw new RuntimeException("IP 归属地查询失败: ret=" . ($data['ret'] ?? 'N/A'));
    }
    return $data['data'];
}

// Redis 频率计数:同 IP 60 秒内抢号次数
function hit_count($redis, $ip) {
    $key = "reg:hit:" . $ip;
    $n = $redis->incr($key);
    if ($n === 1) { $redis->expire($key, 60); }
    return $n;
}

function precheck($redis, $client_ip) {
    $geo = ip_geo($client_ip);
    $isp   = $geo['isp'] ?? '';
    $prov  = $geo['prov'] ?? '';
    $score = 0;

    // 信号 1:机房/企业出口。ip_type 是 VIP 版字段(ISP家庭/BUS企业/IDC机房),
    // 免费版退化用运营商关键词兜底:家庭宽带一般是三大运营商
    $ip_type = $geo['ip_type'] ?? '';
    if ($ip_type === 'IDC' || $ip_type === 'BUS') {
        $score += 3;
    } elseif (!in_array($isp, ['电信', '联通', '移动', '教育网'], true)) {
        $score += 1; // 冷门运营商,先打个问号
    }

    // 信号 2:60 秒内同 IP 高频(正常人手点击远到不了这个数)
    if (hit_count($redis, $client_ip) > 15) {
        $score += 3;
    }

    // 分档
    if ($score >= 4) { return '拒绝'; }     // 机房 IP + 高频:基本是抢号脚本
    if ($score >= 2) { return '验证码'; }   // 可疑,进来了就多验一步
    return '放行';
}

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
echo precheck($redis, $_SERVER['REMOTE_ADDR'] ?? '') . PHP_EOL;

代码里三个环节是关键:ip_geo() 把「请求 IP → 省市/运营商/类型」一次查清楚,是后面所有规则的数据底座;hit_count() 用 Redis 原子自增做滑动窗口计数,多台挂号服务器共享同一份计数,不会因为负载均衡漏判;precheck() 里的打分是规则引擎的雏形,阈值可以配在配置中心,别写死在代码里。

四、落地注意事项 ​

  • 误伤兜底:机房 IP 不等于黄牛——部分用户走公司 Wi-Fi 或云电脑挂号,直接拒绝会把真实患者挡在门外。稳妥做法是高危档也不硬拒,改成「滑块验证 + 队列靠后」,让真人能过、脚本成本变高。
  • 阈值要调:15 次/60 秒是我给的示例,上线前拿一周正常挂号日志回放标定,看正常用户 95 分位在哪,再定阈值。
  • 多因子一起上:IP 只是第一道闸。配合设备指纹、手机号实名年龄校验、抢号成功后的号源二次核验(就诊时人脸+证件),才闭环。号源是刚性资源,单靠 IP 挡不住铁了心的黄牛。
  • 合规提醒:IP 属地区县、IP 类型这类信息属于个人信息处理范畴,建议只用于反欺诈目的、最小化留存(日志脱敏、定期清理),并在隐私政策里写明。
  • 性能:免费版限 60 次/分钟/IP,医院放号瞬间是并发的,别在用户请求链路上每次都直连查询——把查询结果按 IP 段缓存 10 分钟,或直接升级 VIP(18 万次/分钟)扛高峰。

总结 ​

挂号风控的思路是「把最贵的资源留给最可能的真人」:放号前用 IP 归属地把机房代理和高频脚本先筛掉一层,可疑的加验证码,正常家庭宽带的用户基本无感。接口用 https://ip9.com.cn/get?ip=<客户端IP>,免费版就够起步(支持 IPv4/IPv6,返回省份、城市、运营商),要 ip_type(ISP/BUS/IDC)、区县和更高并发再上 VIP。先跑通预检 + 频率计数,再逐步加因子,比一次性上重系统稳得多。