Skip to content

婚恋交友平台怎么用 IP 属地识别「杀猪盘」账号?Java 做线索聚合 ​

用户投诉到客服:聊了三个月的「投行男」,自称在上海陆家嘴办公,聊到「跟着老师做单」才发现不对。客服翻了账号档案——注册 IP 在境外,最近三十次登录挂了两个东南亚节点,所在地写「上海」。这条线索不是靠用户举报才有的,它一直躺在登录日志里。

一、杀猪盘账号在属地维度上留下的痕迹 ​

婚恋交友类平台有一个绕不开的矛盾:账号是批量养的,人却是「一个一个聊」的。批量养号要效率,效率就留在网络上:

  • 出口 IP 高度集中。同一批账号常常共用几个甚至一个出口,运营商看是一个机房的中转,业务看是「十几个急着找对象的优质男」。
  • 属地与自述矛盾。账号资料写「深圳,金融行业」,登录 IP 长期在境外或者国内另一个省份。话说回来,异地工作、经常出差的人确实存在,所以单点对不上没有意义,要的是「长期对不上」。
  • 属地跳变频繁。一周内登录地跨三四个省,中间没有合理的行程逻辑,通常意味着账号被多人共用或者背后有运营团队在轮值。
  • 注册期与活跃期的属地不一样。批量注册时集中在一个出口,交给「聊手」接手后换成另一批节点,这个切换点在时间轴上看得很明显。

二、为什么说 IP 属地是线索不是证据 ​

这一点必须先讲清楚,不然规则上线一周就会被投诉淹没:

  • 一家人、一栋写字楼、一个园区共用一个出口,几个人同时注册登录完全正常;北上广深的租房青年用同一个小区的宽带更是常态。
  • 境外华人、留学生、长期出差的人属地显示境外或异地,是事实不是疑点。
  • 属地是城市级,接口的经纬度是城市中心点,只能判断「大概在哪个城市」,判不了「同城不同区」这种精细结论。VIP 版能给到区县字段,精度仍是数据粒度意义上的,不能当定位用。
  • 合规红线:IP 属于个人信息,做反诈线索分析要有明确目的、只做聚合与人工复核留痕,别把原始 IP 明细当报表到处发;平台展示用户属地要按现行规则来,也不能因为属地异常就在 App 上给用户打「疑似诈骗」的标签。

所以正确姿势:系统只负责把可疑账号排到人工队列前面,处置全靠人工核验(聊天记录、举报记录、实名信息)后决定。

三、Java 实现:两份线索清单 ​

下面这个单文件程序(Java 21,只用 JDK 自带的 HttpClient,不需要第三方 JSON 库)吃一份账号档案:账号 ID、自述城市、近七天注册/登录 IP,输出「属地与自述矛盾名单」和「同出口聚合名单」。

java
// FraudLeadScan.java —— 婚恋平台「杀猪盘」线索扫描:属地矛盾 + 团伙出口聚合
// 运行: java FraudLeadScan.java
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class FraudLeadScan {

    static final String API = "https://ip9.com.cn/get";
    // 免费版 60 次/分钟,单机串行 + 缓存最省事
    static final Map<String, String> CACHE = new ConcurrentHashMap<>();
    static final HttpClient CLIENT = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(3)).build();
    static long lastCall = 0L;

    /** 账号档案:自述所在城市 + 近 7 天登录/注册 IP */
    record Account(String id, String claimCity, List<String> ips) {}

    static synchronized String region(String ip) {
        if (CACHE.containsKey(ip)) return CACHE.get(ip);
        long gap = System.currentTimeMillis() - lastCall;
        if (gap < 1050) {
            try { Thread.sleep(1050 - gap); } catch (InterruptedException ignored) {}
        }
        lastCall = System.currentTimeMillis();
        try {
            HttpRequest req = HttpRequest.newBuilder(URI.create(API + "?ip=" + ip))
                    .timeout(Duration.ofSeconds(3))
                    .header("User-Agent", "fraud-lead-scan/1.0").build();
            HttpResponse<String> resp = CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
            String body = resp.body();                       // {"ret":200,"data":{...}}
            if (!body.contains("\"ret\":200")) return "未知";
            String prov = pick(body, "prov"), city = pick(body, "city"),
                   country = pick(body, "country"), isp = pick(body, "isp");
            if (country.contains("保留") || isp.contains("地址")) return "本地网络";
            String label = !city.isEmpty() ? prov + " " + city
                         : !prov.isEmpty() ? prov
                         : country;
            CACHE.put(ip, label);
            return label;
        } catch (Exception e) {
            return "未知";                                   // 超时/网络异常统一降级
        }
    }

    /** 极简字段提取,避免为了一次解析引入第三方 JSON 库 */
    static String pick(String json, String key) {
        int i = json.indexOf("\"" + key + "\":\"");
        if (i < 0) return "";
        int s = i + key.length() + 4, e = json.indexOf('"', s);
        return e < 0 ? "" : json.substring(s, e);
    }

    public static void main(String[] args) {
        List<Account> accounts = List.of(
            new Account("u_88312", "上海", List.of("45.11.45.11", "45.11.45.11", "103.75.190.2")),
            new Account("u_88377", "深圳", List.of("120.79.4.66", "120.79.4.66")),
            new Account("u_88401", "深圳", List.of("120.79.4.66", "223.5.5.5")),
            new Account("u_88455", "北京", List.of("120.79.4.66", "120.79.4.66")),
            new Account("u_88590", "杭州", List.of("114.114.114.114"))
        );

        System.out.println("== 账号属地画像 ==");
        Map<String, List<String>> ipOwners = new LinkedHashMap<>();   // IP -> 账号
        for (Account a : accounts) {
            Set<String> regions = new LinkedHashSet<>();
            for (String ip : a.ips) {
                regions.add(region(ip));
                ipOwners.computeIfAbsent(ip, k -> new ArrayList<>()).add(a.id());
            }
            boolean mismatch = regions.stream()
                    .filter(r -> !r.equals("未知") && !r.equals("本地网络"))
                    .noneMatch(r -> r.contains(a.claimCity()));
            System.out.printf("%-9s 自述:%-4s 实际:%s%s%n", a.id(), a.claimCity(),
                    String.join("/", regions), mismatch ? "   <== 属地与自述矛盾" : "");
        }

        System.out.println("\n== 同出口 IP 聚合(团伙养号 / 代运营) ==");
        ipOwners.forEach((ip, owners) -> {
            if (owners.size() >= 2) {
                System.out.printf("%-15s %s => %d 个账号%n", ip, region(ip), owners.size());
            }
        });
    }
}

java FraudLeadScan.java 跑出来的真实结果:

== 账号属地画像 ==
u_88312   自述:上海   实际:德国/ 吉隆坡   <== 属地与自述矛盾
u_88377   自述:深圳   实际:广东 深圳
u_88401   自述:深圳   实际:广东 深圳/浙江 杭州
u_88455   自述:北京   实际:广东 深圳   <== 属地与自述矛盾
u_88590   自述:杭州   实际:江苏 南京   <== 属地与自述矛盾

== 同出口 IP 聚合(团伙养号 / 代运营) ==
45.11.45.11     德国 => 2 个账号
120.79.4.66     广东 深圳 => 5 个账号

u_88312 这行是典型画像:自述上海,实际出口一个在德国一个在马来西亚,先看聊天记录再看实名,人工十分钟能定性。u_88401 那行则不能急着下结论——深圳和杭州都出现,可能是本人出差,也可能被别人登录过,这类要去看登录时间是否连续、设备是否变化。

代码里有几个工程上的取舍:

  • region() 用 synchronized + lastCall 做全局限速,1.05 秒一次,正好压在免费版 60 次/分钟下面;真上生产建议先按账号量估算,日均几千条直接考虑 VIP 版(18 万次/分钟),别用免费额度硬扛。
  • 自己写 pick() 提取字段是为了不给示例引入 Gson/Jackson 依赖,生产项目里当然直接用 JSON 库,ret 不等于 200(400 非法 IP、401、429 限流)时按失败处理。
  • 返回「本地网络」「未知」的账号不参与矛盾判定,避免把内网采集故障当成可疑行为——很多平台的误报就是从这一步来的。

四、把线索变成业务动作 ​

跑出名单只是第一步,后面三件事决定这套东西能不能长期用:

第一,规则要叠多层。 属地只是其中一维。把「属地与自述长期矛盾」+「同出口账号数 ≥ N」+「注册 7 天内就提投资/交易话题」叠在一起,命中率会明显提升,单靠属地会误伤长期异地工作的用户。

第二,人在回路。 所有命中记录进人工队列,客服看聊天记录后决定提示、限制还是移交。别做自动封号——婚恋平台的封号成本比电商高得多,用户投诉「正常交友被误封」的伤害远大于晚处理几天。

第三,合规留痕。 原始 IP 明细设留存期限,导出报表只保留聚合后的省份分布和账号数;属地相关字段在内部系统里的可见范围要收窄。投诉处理时,属地线索可以作为佐证写进记录,但不能当唯一结论。

总结 ​

杀猪盘的技术特征不神秘,就是「批量」两个字——批量注册、批量登录、批量换节点,而这些动作必然在同一批出口 IP 上留下脚印。用 https://ip9.com.cn/get?ip=<ip> 把登录日志里的 IP 翻成省市(免费版够用,不需要注册,支持 IPv4/IPv6,限制是 60 次/分钟,批量跑注意限速和缓存),再用几十行 Java 把「属地与自述矛盾」和「同出口聚合」两份名单打出来,反诈团队的效率至少提一个量级。要更细的区县粒度和 ip_type(区分家庭宽带和机房出口)、ip_asn(判断出口属于哪家云厂商)字段,看官网 https://www.ip9.com.cn 的 VIP 版说明。