Appearance
婚恋交友平台怎么用 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 版说明。