refactor(ip-region): 移除 ip2region 旧版区域字段兼容, 统一 v4.xdb 五段格式并补单测

This commit is contained in:
daxpay
2026-08-06 14:35:05 +08:00
parent 853530e490
commit df997d0a28
6 changed files with 127 additions and 17 deletions

View File

@@ -64,10 +64,10 @@ public class IpRegionDemoController {
IpRegion region = ipToRegionService.getRegionByIp(ip);
if (Objects.nonNull(region)) {
result.setCountry(region.getCountry())
.setRegion(region.getRegion())
.setProvince(region.getProvince())
.setCity(region.getCity())
.setIsp(region.getIsp())
.setCountryCode(region.getCountryCode())
.setInnerIp(region.isInnerIp())
.setChinaIp(region.isChinaIp());
}

View File

@@ -17,9 +17,6 @@ public class IpRegionDemoResult {
/// 国家
private String country;
/// 区域
private String region;
/// 省份
private String province;
@@ -29,6 +26,9 @@ public class IpRegionDemoResult {
/// ISP 运营商
private String isp;
/// 国家码(iso-alpha2, 如 CN/HK/US)
private String countryCode;
/// 格式化后的归属地文本(与审计日志 location 字段同源)
private String regionStr;

View File

@@ -71,6 +71,13 @@
<artifactId>common-translate</artifactId>
<version>${project.version}</version>
</dependency>
<!-- 测试包 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -8,6 +8,7 @@ import java.util.List;
/// # IP对应地址区域信息
///
/// 官方 ip2region v4.xdb 数据格式: `国家|省份|城市|ISP|国家码(iso-alpha2)`, 五段定长。
@Data
@Accessors(chain = true)
public class IpRegion {
@@ -17,18 +18,18 @@ public class IpRegion {
/// 国家
private String country;
/// 区域
private String region;
/// 省份
private String province;
/// 城市
private String city;
/// ISP
/// ISP 运营商
private String isp;
/// 国家码(iso-alpha2, v4 末尾段, 如 CN/HK/US)
private String countryCode;
/// 是否内网地址
public boolean isInnerIp(){
return "内网IP".equals(isp);
@@ -40,30 +41,51 @@ public class IpRegion {
}
/// 是否国内直辖市
///
/// v4 数据省名可能带"市"后缀(如"北京市"), 归一化后比对, 与 GeoFenceUtil.normalizeRegionName 口径一致
public boolean isProvinceLevel(){
return "中国".equals(country)&&
PROVINCE_LEVEL_CITY.contains(province);
PROVINCE_LEVEL_CITY.contains(normalizeName(province));
}
/// 是否港澳台
public boolean isBigChina(){
return "中国".equals(country)&&
BIG_CHINA.contains(province);
BIG_CHINA.contains(normalizeName(province));
}
/// 国家|区域|省份|城市|ISP
/// 官方 v4.xdb 格式: 国家|省份|城市|ISP|国家码
///
/// 防御性取值: 记录段数可能不足(国外/内网等短记录), 逐段判空, 不足不抛异常
public static IpRegion init(List<String> ipInfo){
IpRegion ipRegion = new IpRegion();
if (CollUtil.isEmpty(ipInfo)){
return ipRegion;
}
ipRegion.country = ipInfo.get(0);
ipRegion.region = ipInfo.get(1);
ipRegion.province = ipInfo.get(2);
ipRegion.city = ipInfo.get(3);
ipRegion.isp = ipInfo.get(4);
ipRegion.country = safeGet(ipInfo, 0);
ipRegion.province = safeGet(ipInfo, 1);
ipRegion.city = safeGet(ipInfo, 2);
ipRegion.isp = safeGet(ipInfo, 3);
ipRegion.countryCode = safeGet(ipInfo, 4);
return ipRegion;
}
/// 安全取值: 越界返回 null 而非抛异常
private static String safeGet(List<String> ipInfo, int index) {
return index < ipInfo.size() ? ipInfo.get(index) : null;
}
/// 归一化地区名: 去首尾空格, 去掉结尾一个"市"或"省"
private static String normalizeName(String name) {
if (name == null) {
return "";
}
String result = name.trim();
if (result.endsWith("") || result.endsWith("")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
}

View File

@@ -100,7 +100,7 @@ public class IpToRegionService {
}
try {
// 城市Id|国家|区域|省份|城市|ISP
// 官方 v4.xdb 格式: 国家|省份|城市|ISP|国家码(iso-alpha2)
String search = searcher.search(ip);
List<String> ipInfo = StrUtil.split(search, "|");
return IpRegion.init(ipInfo);

View File

@@ -0,0 +1,81 @@
package cn.daxpay.open.platform.capability.audit.log.service.ip2region;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/// # IpRegion 解析单元测试
///
/// 锁定官方 ip2region v4.xdb 五段格式 `国家|省份|城市|ISP|国家码` 的字段映射,
/// 防止再次退化为旧版 v2/v3 的 `国家|区域|省份|城市|ISP` 错位解析。
class IpRegionTest {
@Test
@DisplayName("v4 标准五段记录: 字段映射正确")
void init_standardV4Record_shouldMapFields() {
// 国家|省份|城市|ISP|国家码
IpRegion region = IpRegion.init(List.of("中国", "山东省", "济南市", "电信", "CN"));
assertEquals("中国", region.getCountry());
assertEquals("山东省", region.getProvince());
assertEquals("济南市", region.getCity());
assertEquals("电信", region.getIsp());
assertEquals("CN", region.getCountryCode());
}
@Test
@DisplayName("空记录: 返回空对象不抛异常")
void init_emptyList_shouldReturnBlank() {
IpRegion region = IpRegion.init(List.of());
assertNull(region.getCountry());
assertNull(region.getProvince());
assertNull(region.getCity());
assertNull(region.getIsp());
assertNull(region.getCountryCode());
}
@Test
@DisplayName("短记录(3段): 防御取值, 后段为 null 不抛异常")
void init_shortRecord_shouldNotThrow() {
IpRegion region = IpRegion.init(List.of("中国", "山东省", "济南市"));
assertEquals("中国", region.getCountry());
assertEquals("山东省", region.getProvince());
assertEquals("济南市", region.getCity());
assertNull(region.getIsp());
assertNull(region.getCountryCode());
}
@Test
@DisplayName("国内普通城市: isChinaIp=true, isProvinceLevel=false")
void isChinaIp_normalCity_shouldPass() {
IpRegion region = IpRegion.init(List.of("中国", "山东省", "济南市", "电信", "CN"));
assertTrue(region.isChinaIp());
assertFalse(region.isProvinceLevel());
}
@Test
@DisplayName("直辖市(省名带市后缀): isProvinceLevel 归一化后识别")
void isProvinceLevel_directCityWithSuffix_shouldDetect() {
// v4 数据省名可能带"市"后缀
IpRegion region = IpRegion.init(List.of("中国", "北京市", "北京", "电信", "CN"));
assertTrue(region.isProvinceLevel());
}
@Test
@DisplayName("港澳台: isBigChina 识别")
void isBigChina_hk_shouldDetect() {
IpRegion region = IpRegion.init(List.of("中国", "香港", "香港", "HKT", "HK"));
assertTrue(region.isBigChina());
}
}