Appearance
医院在线挂号怎么防黄牛?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。先跑通预检 + 频率计数,再逐步加因子,比一次性上重系统稳得多。