Skip to content

二手交易平台怎么防「假同城」?用 IP 归属地校验发布地和聊天属地(PHP 实现) ​

「同城自提,当面验货」这六个字在二手交易里几乎是信任的代名词。买家愿意多跑十几公里,图的就是能看得见摸得着、不用先付款。也正因为这层信任,它成了灰产最好用的外衣。

套路很固定:商品标着「北京朝阳自提」,价格比同类低一截,买家一问,对方说「我出差了,让我朋友给你寄过去吧,你先付个定金」。等到定金打完,人就没影了。这类骗局里,卖家账号的实际登录位置常常离标注城市上千公里,甚至根本不在境内。

IP 归属地不能证明一个人是谁,但它能证明「这次发布、这次聊天从哪个城市发出」。二手平台的风控基本都会拿这条线索做交叉比对。这篇用 PHP 给一套完整的校验规则,包含发布地校验、私聊属地校验和分级处置。

一、场景问题:假同城的三种表现 ​

第一种,标同城、人不在同城。 商品标注在 A 城,卖家发布那一刻的 IP 在 B 城甚至境外。买家看不到这层信息,只看到一个「同城自提」的标签,风险完全被隐藏了。

第二种,发布时在同城,聊天时人跑了。 更隐蔽的一种。发布的时候人确实在本市(可能刚出差回来,或者干脆是买了张本地卡),等到买家来咨询,已经换到别的城市甚至境外了。聊天记录里那句「我就在本地,随时可自提」和实际的聊天 IP 对不上,这本身就是最有价值的信号。

第三种,一个出口 IP 铺了几十个卖家账号。 单个账号看都正常,价格便宜、图片齐全、描述也像模像样。但它们的发布 IP 高度集中 —— 这是工作室批量铺货的典型特征,背后往往是一台服务器或者一个群控机房。

需要提前说清楚:这三种形态都是线索,不是定罪。二手平台上真实的异地场景太多了 —— 学生放假回老家还在卖东西、出差的人在酒店发帖、帮父母卖闲置的年轻人。所以规则的设计目标是把可疑商品挑出来提醒买家、降低推荐权重,而不是自动下架或者封号。

二、方案设计 ​

数据来源。 商品表拿标注城市,访问日志拿发布 IP 和私聊 IP,归属地统一走 GET https://ip9.com.cn/get?ip=<ip>,带文件缓存(归属地变化慢,缓存 7 天)和 1 秒 1 次的限速(守住免费版 60 次/分钟的额度)。

规则组织。 每条规则只输出「命中/不命中 + 权重变化」,最后按总权重落处置档位。规则和处置分开的好处是:调阈值的时候不用改规则代码,风控同学自己就能试。

规则条件权重
R1发布 IP 城市 ≠ 商品标注城市(境内)-30
R2商品标注同城,但发布 IP 归属境外-50
R3私聊 IP 城市均不在标注城市-20
R3b私聊 IP 归属境外-20
R4同一出口 IP 关联 ≥5 个卖家账号-25
R5发布地与聊天地跨省记录(不扣权)

处置分档。 权重 ≤ -50:去掉同城标签,同时在聊天页给买家弹强提醒;≤ -20:同城板块曝光降权,只在聊天页写一行提示;> -20:正常展示,仅在后台留下研判记录。

三、PHP 实现 ​

查询封装 + 规则引擎(fake_local_check.php,只依赖 PHP 标准库,HTTPS 走 openssl,不需要 curl 扩展):

php
<?php
const IP9_API     = 'https://ip9.com.cn/get';
const CACHE_FILE  = __DIR__ . '/ip_cache.json';
const CACHE_TTL   = 7 * 86400;
const RATE_USLEEP = 1000000;   // 免费版 60 次/分钟 -> 1 秒 1 次

/** 带文件缓存 + 限速的归属地查询;失败结果只缓存 60 秒 */
function ip9_lookup(string $ip): array
{
    static $cache = null, $lastAt = 0.0;
    if ($cache === null) {
        $cache = file_exists(CACHE_FILE) ? json_decode(file_get_contents(CACHE_FILE), true) : [];
        if (!is_array($cache)) { $cache = []; }
    }
    if (isset($cache[$ip])) {
        $ttl = $cache[$ip]['ok'] ? CACHE_TTL : 60;
        if (time() - $cache[$ip]['ts'] < $ttl) {
            $cache[$ip]['hit'] = true;
            return $cache[$ip];
        }
    }

    $ctx = stream_context_create([
        'http' => ['method' => 'GET', 'timeout' => 6, 'ignore_errors' => true,
                   'header' => "User-Agent: fakelocal/1.0\r\nAccept: application/json\r\n"],
        'ssl'  => ['verify_peer' => true, 'verify_peer_name' => true],
    ]);
    $body = @file_get_contents(IP9_API . '?ip=' . rawurlencode($ip), false, $ctx);

    $rec = ['ts' => time(), 'hit' => false, 'ok' => false, 'country' => '', 'prov' => '',
            'city' => '', 'isp' => '', 'err' => ''];
    if ($body === false) {
        $rec['err'] = '请求失败或超时';
    } else {
        $j = json_decode($body, true);
        if (!is_array($j)) {
            $rec['err'] = '响应不是 JSON';
        } elseif ((int)($j['ret'] ?? 0) !== 200) {
            $rec['err'] = 'ret=' . ($j['ret'] ?? '?');
        } else {
            $d = $j['data'] ?? [];
            $rec['ok']      = true;
            $rec['country'] = (string)($d['country'] ?? '');
            $rec['prov']    = (string)($d['prov'] ?? '');
            $rec['city']    = (string)($d['city'] ?? '');
            $rec['isp']     = (string)($d['isp'] ?? '');
        }
    }
    $cache[$ip] = $rec;
    file_put_contents(CACHE_FILE, json_encode($cache, JSON_UNESCAPED_UNICODE));

    $now = microtime(true);
    if ($now - $lastAt < 1.0) { usleep(RATE_USLEEP); }
    $lastAt = microtime(true);
    return $rec;
}

规则部分,输入是「商品信息 + 发布 IP 的归属地 + 私聊 IP 的归属地数组」:

php
function judge(array $item, array $pubGeo, array $chatGeos, array $ipSellerCount): array
{
    $hits = [];
    $weight = 0;
    $notifyBuyer = false;

    $declared = $item['declared_city'];
    $internalPub = $pubGeo['ok'] && ($pubGeo['country'] === '保留' || $pubGeo['isp'] === '内网地址');

    // R1 发布 IP 城市与商品标注城市不一致
    if ($pubGeo['ok'] && !$internalPub && $pubGeo['country'] === '中国'
        && $pubGeo['city'] !== '' && $pubGeo['city'] !== $declared) {
        $weight -= 30;
        $hits[] = "发布 IP 在{$pubGeo['prov']}{$pubGeo['city']},商品标注在{$declared}";
    }

    // R2 标注同城但发布 IP 在境外
    if ($pubGeo['ok'] && !$internalPub && $pubGeo['country'] !== '中国'
        && $pubGeo['country'] !== '保留') {
        $weight -= 50;
        $notifyBuyer = true;
        $hits[] = "商品标注{$declared},但发布 IP 归属{$pubGeo['country']}";
    }

    // R3 私聊过程中出现与标注城市不同的 IP
    $chatCities = [];
    foreach ($chatGeos as $g) {
        if ($g['ok'] && $g['country'] === '中国' && $g['city'] !== '') {
            $chatCities[$g['city']] = true;
        }
    }
    if ($chatCities && !isset($chatCities[$declared])) {
        $weight -= 20;
        $notifyBuyer = true;
        $hits[] = '聊天 IP 城市为 ' . implode('/', array_keys($chatCities)) . ",均不在{$declared}";
    }

    // R3b 私聊 IP 归属境外(商品标的是国内同城)
    $chatOverseas = [];
    foreach ($chatGeos as $g) {
        if ($g['ok'] && $g['country'] !== '' && $g['country'] !== '中国'
            && $g['country'] !== '保留') {
            $chatOverseas[$g['country']] = true;
        }
    }
    if ($chatOverseas) {
        $weight -= 20;
        $notifyBuyer = true;
        $hits[] = '聊天 IP 归属 ' . implode('/', array_keys($chatOverseas)) . ',不在境内';
    }

    // R4 同一 IP 关联大量卖家账号(工作室批量铺货)
    if (($ipSellerCount[$item['seller_ip']] ?? 1) >= 5) {
        $weight -= 25;
        $hits[] = '同一出口 IP 关联 ' . $ipSellerCount[$item['seller_ip']] . ' 个卖家账号';
    }

    // R5 发布地与聊天地跨省:只记录
    if ($pubGeo['ok'] && $chatCities && $pubGeo['city'] !== ''
        && !isset($chatCities[$pubGeo['city']])) {
        $hits[] = "发布时在{$pubGeo['city']},聊天时到了" . implode('/', array_keys($chatCities)) . '(记录,不额外降权)';
    }

    // 拿不到数据:只记录原因,不参与研判
    if (!$pubGeo['ok']) {
        $hits[] = '发布 IP 无法定位:' . ($pubGeo['err'] ?: '未知') . '(不参与研判)';
    } elseif ($internalPub) {
        $hits[] = '发布 IP 是内网/保留地址(' . $pubGeo['isp'] . '),不做地域校验';
    }

    $action = '正常展示';
    if ($weight <= -50)     { $action = '去掉同城标签 + 前端强提醒买家'; }
    elseif ($weight <= -20) { $action = '同城曝光降权 + 聊天页提醒买家'; }

    return ['weight' => $weight, 'hits' => $hits, 'action' => $action, 'notify' => $notifyBuyer];
}

喂进去的商品样例(items.json,每个商品带发布 IP 和最近几条私聊的 IP):

json
[
  {"item_id":"IT-1006","title":"二手自行车","seller_id":"S-81","seller_ip":"114.114.114.114","declared_city":"南京","chat_ips":["114.114.114.114"]},
  {"item_id":"IT-1007","title":"iPad 港版自提","seller_id":"S-82","seller_ip":"45.153.160.2","declared_city":"南京","chat_ips":["45.153.160.2"]},
  {"item_id":"IT-1008","title":"显卡自提当面验","seller_id":"S-83","seller_ip":"58.213.1.1","declared_city":"南京","chat_ips":["45.153.160.2"]},
  {"item_id":"IT-1009","title":"相机镜头同城面交","seller_id":"S-84","seller_ip":"180.101.49.12","declared_city":"上海","chat_ips":["223.5.5.5"]}
]

四、实测输出 ​

11 件商品、7 个卖家出口 IP 跑下来(下面摘了几条有代表性的):

待研判商品 11 件,涉及 7 个卖家出口 IP

IT-1006  二手自行车 标同城 南京 | 发布 IP 114.114.114.114  -> 江苏南京
         同城权重 0 | 处置:正常展示

IT-1007  iPad 港版自提 标同城 南京 | 发布 IP 45.153.160.2
         同城权重 -70 | 处置:去掉同城标签 + 前端强提醒买家
         · 商品标注南京,但发布 IP 归属捷克
         · 聊天 IP 归属 捷克,不在境内

IT-1008  显卡自提当面验 标同城 南京 | 发布 IP 58.213.1.1       -> 江苏南京
         同城权重 -20 | 处置:同城曝光降权 + 聊天页提醒买家
         · 聊天 IP 归属 捷克,不在境内

IT-1009  相机镜头同城面交 标同城 上海 | 发布 IP 180.101.49.12    -> 江苏南京
         同城权重 -50 | 处置:去掉同城标签 + 前端强提醒买家
         · 发布 IP 在江苏南京,商品标注在上海
         · 聊天 IP 城市为 杭州,均不在上海
         · 发布时在南京,聊天时到了杭州(记录,不额外降权)

IT-1010  办公椅自提 标同城 深圳 | 发布 IP 192.168.1.10
         同城权重 0 | 处置:正常展示
         · 发布 IP 是内网/保留地址(内网地址),不做地域校验

IT-1011  路由器 标同城 南京 | 发布 IP 999.1.1.1        -> (未定位)
         同城权重 0 | 处置:正常展示
         · 发布 IP 无法定位:ret=400(不参与研判)

首跑 8.3 秒(7 次真实请求,每次间隔 1 秒),全缓存二跑 0.018 秒。几条判定值得展开:

IT-1008 是最值得警惕的一类。 发布 IP 在南京,和标注城市一致,单看发布环节完全正常;但买家来聊的时候,聊天 IP 已经在境外了。这种「发布时人在本地、聊着聊着出国了」的模式,正是「先骗取同城信任、再诱导脱离平台交易」的典型路径,所以它虽然只有 -20 分,也照样给买家弹提醒。

IT-1007 拿满了两条境外规则,-70 分。 发布和聊天都在境外,商品却标着南京同城自提,这种组合基本可以直接去掉同城标签。注意处置仍然是「提醒买家」,不是自动下架 —— 海外华人卖家里也有真实用户。

IT-1009 三条规则叠加到 -50 分。 标注上海、发布在南京、聊天在杭州,三个城市互不相同。这条数据既能说明「账号可能被转手或者有代运营」,也能说明「这个人的位置一直在变」,对买家来说都是值得知道的。

IT-1010 和 IT-1011 都不扣权。 192.168.1.10 是内网地址,接口返回 country=保留、isp=内网地址;999.1.1.1 直接 ret=400。这两类在风控脚本里最容易被写成「查不到 → 高风险」,结果平台自己的测试环境、内网访问、上游代理一亮相就刷一片告警。

R4 那条规则只扣 25 分,故意的。 一个共享出口 IP 挂 5 个卖家账号,可能是工作室,也可能是某个二手小店集中的写字楼群租网络。它单独不足以定性,但和「价格异常低」「商品图片重复」这类信号叠在一起就很有说服力了。

五、落地时要调整的几件事 ​

权重和阈值要按自己的数据调。 上面这套权重是在「二三线城市为主、同城自提占比高的平台」上试出来的。如果你的平台跨省交易本来就是主流,R1 的 30 分就太重了,可以先降权记录一到两周,统计一下日均命中量再定阈值 —— 每天几万条提醒等于没有提醒。

聊天 IP 要看清是哪一个。 App 端和网页端的出口常常不同(同一个人的手机走 4G、电脑走宽带,可能落在两个城市),私聊几分钟内切换城市属于正常现象。真正有信号的是「聊天全程都远离标注城市」或者「短时间内跨省反复横跳」,所以规则里看的是城市的集合,而不是最后一条消息的 IP。

同城标签的「权重」要能被别的信号覆盖。 比如已经完成实人认证、有历史好评、近期成交记录的老卖家,被 R1 扣掉的曝光权重应该能靠账号信用补回来。单靠 IP 一条线索做降权,中长期会伤到平台的头部卖家。

识别工作室要看 ip_type,这是 VIP 版字段。 免费版能告诉你 IP 在哪个城市、哪家运营商,但分辨不了这个出口是家庭宽带还是 IDC 机房。批量铺货的工作室基本都用云主机跑脚本,出口是机房 IP —— 这个判断得靠 ip_type(ISP 家庭 / BUS 企业 / IDC 机房)和 ip_asn,只有 VIP 版提供。建议先用免费版把「同 IP 多账号」这条跑起来,确认真的有这类流量,再决定要不要上 VIP。

页面展示符合合规口径。 给买家看「卖家当前 IP 属地」时,境内展示到省、境外展示到国家就够了,不要展示城市甚至经纬度 —— 接口返回的经纬度是城市中心点,精度本来也不足以定位到人,但展示出来会引发不必要的隐私争议。另外原始 IP 属于个人信息,研判结论存「命中哪条规则」就行,日志里的 IP 记得做掩码。

代理和秒拨是这套规则的天花板。 灰产手里有大量代理 IP,切换属地的成本很低。IP 归属地只是「成本最低、覆盖最广」的第一道筛子,真正要拦住有组织的诈骗,还得叠上设备指纹、支付通道、账号注册时间和买家举报数据。别指望一条 IP 规则解决所有问题。

思路可以套用到所有「线上发布、线下见面」的场景:家政预约、房屋租赁、二手车看车。核心问题永远是同一个 —— 对方说自己在哪里,和他实际的网络位置对不对得上。接口 GET https://ip9.com.cn/get?ip=<ip> 免费版免注册、60 次/分钟/IP,返回 prov、city、isp、big_area、经纬度(城市中心点)这些字段;区县、ip_type、ip_asn 属于 VIP 版能力,具体口径以官网 https://www.ip9.com.cn 为准。