fix(ip): IP 地理位置解析改用 ip2region 本地离线库 (#45)

* fix(ip): IP 地理位置解析改用 ip2region 本地离线库

替换原先依赖外部接口 whois.pconline.com.cn 的实现,消除
"Unexpected end of file from server" 等因外部接口抖动导致的
异常日志与出网调用。改用 org.lionsoul:ip2region 本地 xdb
离线库,全量加载到内存查询,保持 "省-市" 返回格式不变。
This commit is contained in:
ten authored and GitHub committed 2026-05-20 06:49:11 +08:00
1 parent e4843c9a8f
commit d504c2b2f8
4 files changed
+76 -19

No files matched your search

+4
View File
@@ -1,3 +1,7 @@
## Unreleased
- 修复:IP 地理位置解析改用 `ip2region` 本地离线库,移除对外部接口 `whois.pconline.com.cn` 的依赖,消除 "Unexpected end of file from server" 等因外部接口抖动导致的异常日志与出网调用
## 2.1
- 优化:Dockerfile 拆分依赖层,前端先 `COPY package.json/pnpm-lock.yaml` 再 `pnpm i`,Java 先 `COPY pom.xml` 并 `dependency:go-offline` 预热依赖,源码变更不再触发依赖重装,显著缩短增量构建耗时
@@ -15,18 +15,46 @@
*/
package xyz.playedu.common.util;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
@Slf4j
public class IpUtil {
private static final String XDB_CLASSPATH = "ip2region/ip2region.xdb";
private static volatile Searcher ipSearcher;
private static Searcher getSearcher() {
if (ipSearcher == null) {
synchronized (IpUtil.class) {
if (ipSearcher == null) {
try (InputStream in =
IpUtil.class.getClassLoader().getResourceAsStream(XDB_CLASSPATH)) {
if (in == null) {
log.error("ip2region xdb 资源未找到: {}", XDB_CLASSPATH);
return null;
}
ByteArrayOutputStream buf = new ByteArrayOutputStream();
byte[] chunk = new byte[8192];
int n;
while ((n = in.read(chunk)) != -1) {
buf.write(chunk, 0, n);
}
ipSearcher = Searcher.newWithBuffer(buf.toByteArray());
} catch (Exception e) {
log.error("初始化 ip2region 失败 msg={}", e.getMessage());
}
}
}
}
return ipSearcher;
}
/**
* 获取客户端IP
*
@@ -67,35 +95,54 @@ public class IpUtil {
* @author fzr
*/
public static String getRealAddressByIP(String ip) {
String IP_URL = "https://whois.pconline.com.cn/ipJson.jsp";
String UNKNOWN = "未知";
if (StringUtil.isEmpty(ip)) {
return UNKNOWN;
}
if (IpUtil.internalIp(ip)) {
return "内网";
}
Searcher searcher = getSearcher();
if (searcher == null) {
return UNKNOWN;
}
try {
String rspStr =
HttpUtil.get(
IP_URL,
new HashMap<>() {
{
put("ip", ip);
put("json", true);
}
});
if (StringUtil.isEmpty(rspStr)) {
log.error("获取地理位置异常1 {}", ip);
// xdb 返回格式:国家|省份|城市|ISP|国家代码,例如 "中国|浙江省|杭州市|电信|CN"
String region = searcher.search(ip);
if (StringUtil.isEmpty(region)) {
return UNKNOWN;
}
JSONObject json = JSONUtil.parseObj(rspStr);
return String.format("%s-%s", json.getStr("pro"), json.getStr("city"));
String[] parts = region.split("\\|", -1);
String country = parts.length > 0 ? cleanField(parts[0]) : "";
String pro = parts.length > 1 ? cleanField(parts[1]) : "";
String city = parts.length > 2 ? cleanField(parts[2]) : "";
if (!StringUtil.isEmpty(pro) && !StringUtil.isEmpty(city)) {
return String.format("%s-%s", pro, city);
}
if (!StringUtil.isEmpty(pro)) {
return pro;
}
if (!StringUtil.isEmpty(country)) {
return country;
}
return UNKNOWN;
} catch (Exception e) {
log.error("获取地理位置异常2 {} msg {}", ip, e.getMessage());
log.warn("获取地理位置异常 ip={} msg={}", ip, e.getMessage());
}
return UNKNOWN;
}
private static String cleanField(String s) {
if (s == null || "0".equals(s)) {
return "";
}
return s.trim();
}
/**
* 检查是否为内部IP地址
*
+6
View File
@@ -109,6 +109,12 @@
<version>5.8.32</version>
</dependency>
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>