diff --git a/daxpay-demo/src/main/java/cn/daxpay/open/demo/ipregion/controller/IpRegionDemoController.java b/daxpay-demo/src/main/java/cn/daxpay/open/demo/ipregion/controller/IpRegionDemoController.java index 1fba4f521..bb1c882ef 100644 --- a/daxpay-demo/src/main/java/cn/daxpay/open/demo/ipregion/controller/IpRegionDemoController.java +++ b/daxpay-demo/src/main/java/cn/daxpay/open/demo/ipregion/controller/IpRegionDemoController.java @@ -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()); } diff --git a/daxpay-demo/src/main/java/cn/daxpay/open/demo/ipregion/result/IpRegionDemoResult.java b/daxpay-demo/src/main/java/cn/daxpay/open/demo/ipregion/result/IpRegionDemoResult.java index af405ad43..4bdff2462 100644 --- a/daxpay-demo/src/main/java/cn/daxpay/open/demo/ipregion/result/IpRegionDemoResult.java +++ b/daxpay-demo/src/main/java/cn/daxpay/open/demo/ipregion/result/IpRegionDemoResult.java @@ -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; diff --git a/daxpay-platform/daxpay-platform-capability/capability-audit-log/pom.xml b/daxpay-platform/daxpay-platform-capability/capability-audit-log/pom.xml index 951801881..d6f2f6817 100644 --- a/daxpay-platform/daxpay-platform-capability/capability-audit-log/pom.xml +++ b/daxpay-platform/daxpay-platform-capability/capability-audit-log/pom.xml @@ -71,6 +71,13 @@ common-translate ${project.version} + + + + org.junit.jupiter + junit-jupiter + test + diff --git a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpRegion.java b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpRegion.java index 4b42ae715..b3f4d0fa3 100644 --- a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpRegion.java +++ b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpRegion.java @@ -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 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 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; + } } diff --git a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpToRegionService.java b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpToRegionService.java index 696c4b625..48cd28def 100644 --- a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpToRegionService.java +++ b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/main/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpToRegionService.java @@ -100,7 +100,7 @@ public class IpToRegionService { } try { - // 城市Id|国家|区域|省份|城市|ISP + // 官方 v4.xdb 格式: 国家|省份|城市|ISP|国家码(iso-alpha2) String search = searcher.search(ip); List ipInfo = StrUtil.split(search, "|"); return IpRegion.init(ipInfo); diff --git a/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/test/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpRegionTest.java b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/test/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpRegionTest.java new file mode 100644 index 000000000..121a2956b --- /dev/null +++ b/daxpay-platform/daxpay-platform-capability/capability-audit-log/src/test/java/cn/daxpay/open/platform/capability/audit/log/service/ip2region/IpRegionTest.java @@ -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()); + } +}