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());
+ }
+}