mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-12 07:25:39 +08:00
refactor(risk): 省市地区拦截合并为地区拦截开关并新增 IPv6 地区匹配
- provinceBlacklistEnabled/cityBlacklistEnabled 合并为 regionBlacklistEnabled,旧字段 @Deprecated 兼容迁移(jsonb 读取时自动合并清理) - 新增 ipv6MatchEnabled 开关,IPv6 地域检查默认关闭(NetUtil.isInnerIP 仅支持 IPv4 需分支隔离) - 省级名单命中后不再执行市级检查,一次 ip2region 解析同时完成省市两级匹配 - 重写 DefaultPayRiskChecker 单测(41 个),验证合并迁移与 IPv6 分支
This commit is contained in:
@@ -72,17 +72,18 @@ public class PayRiskCheckContext {
|
||||
/// 读取平台配置后注入, 供检查器按 IP 归属地判定是否命中。
|
||||
private Boolean blockOverseasIp;
|
||||
|
||||
/// 是否启用省级地区拦截(null/false=不检查;true=按 IP 省份匹配省级黑名单)
|
||||
/// 是否启用地区拦截(null/false=不检查;true=按 IP 归属地匹配省级与市级黑名单)
|
||||
///
|
||||
/// 地域策略开关快照, 由 [cn.daxpay.open.payment.trade.runtime.service.pay.common.PayRiskAssistService]
|
||||
/// 读取平台配置后注入。省级命中后不再执行市级检查。
|
||||
private Boolean provinceBlacklistEnabled;
|
||||
/// 读取平台配置后注入。合并原省级/市级独立开关, 省级命中后不再执行市级检查。
|
||||
private Boolean regionBlacklistEnabled;
|
||||
|
||||
/// 是否启用市级地区拦截(null/false=不检查;true=按 IP 城市匹配市级黑名单)
|
||||
/// 是否启用 IPv6 地区匹配(null/false=IPv6 直通放行;true=对 IPv6 执行地区匹配)
|
||||
///
|
||||
/// 地域策略开关快照, 由 [cn.daxpay.open.payment.trade.runtime.service.pay.common.PayRiskAssistService]
|
||||
/// 读取平台配置后注入。与省级开关独立, 省级未命中时执行。
|
||||
private Boolean cityBlacklistEnabled;
|
||||
/// 读取平台配置后注入。关闭时省级/市级/海外/地理围栏四项检查对 IPv6 直通放行;
|
||||
/// 开启时 IPv6 走 ip2region 查询(xdb 离线数据精度有限)。
|
||||
private Boolean ipv6MatchEnabled;
|
||||
|
||||
/// 是否启用地理围栏(null/false=不检查;true=启用门店市级围栏检测)
|
||||
///
|
||||
|
||||
@@ -72,10 +72,10 @@ public class PayRiskAssistService {
|
||||
ctx.setBlacklistEnabled(config.getBlacklistEnabled());
|
||||
// 海外 IP 拦截开关(地域策略)
|
||||
ctx.setBlockOverseasIp(config.getBlockOverseasIp());
|
||||
// 省级地区拦截开关(地域策略)
|
||||
ctx.setProvinceBlacklistEnabled(config.getProvinceBlacklistEnabled());
|
||||
// 市级地区拦截开关(地域策略, 独立于省级)
|
||||
ctx.setCityBlacklistEnabled(config.getCityBlacklistEnabled());
|
||||
// 地区拦截开关(地域策略, 含省级+市级, 省级命中后不执行市级检查)
|
||||
ctx.setRegionBlacklistEnabled(config.getRegionBlacklistEnabled());
|
||||
// IPv6 地区匹配开关(地域策略, 默认关闭, 关闭时 IPv6 直通放行)
|
||||
ctx.setIpv6MatchEnabled(config.getIpv6MatchEnabled());
|
||||
// 地理围栏两级门控 + 策略 + 放行城市集合(第三层)
|
||||
applyGeoFence(ctx, config, payParam.getMchNo());
|
||||
checker.checkBeforePay(ctx);
|
||||
@@ -97,10 +97,10 @@ public class PayRiskAssistService {
|
||||
PayRiskCheckContext ctx = buildContextFromTrade(trade);
|
||||
// 海外 IP 拦截开关(地域策略, 事后仅记录海外访问)
|
||||
ctx.setBlockOverseasIp(config.getBlockOverseasIp());
|
||||
// 省级地区拦截开关(地域策略, 事后补录)
|
||||
ctx.setProvinceBlacklistEnabled(config.getProvinceBlacklistEnabled());
|
||||
// 市级地区拦截开关(地域策略, 事后补录, 独立于省级)
|
||||
ctx.setCityBlacklistEnabled(config.getCityBlacklistEnabled());
|
||||
// 地区拦截开关(地域策略, 事后补录, 含省级+市级)
|
||||
ctx.setRegionBlacklistEnabled(config.getRegionBlacklistEnabled());
|
||||
// IPv6 地区匹配开关(地域策略, 事后补录, 默认关闭)
|
||||
ctx.setIpv6MatchEnabled(config.getIpv6MatchEnabled());
|
||||
// 地理围栏两级门控 + 策略 + 放行城市集合(第三层, 事后补录)
|
||||
applyGeoFence(ctx, config, trade.getMchNo());
|
||||
// 黑名单拦截开关(第一层, 事后补录)
|
||||
|
||||
@@ -811,11 +811,11 @@
|
||||
"blockOverseasIp": {
|
||||
"notNull": "Overseas IP block toggle cannot be empty"
|
||||
},
|
||||
"provinceBlacklistEnabled": {
|
||||
"notNull": "Province-level blocking toggle cannot be empty"
|
||||
"regionBlacklistEnabled": {
|
||||
"notNull": "Region blocking toggle cannot be empty"
|
||||
},
|
||||
"cityBlacklistEnabled": {
|
||||
"notNull": "City-level blocking toggle cannot be empty"
|
||||
"ipv6MatchEnabled": {
|
||||
"notNull": "IPv6 region matching toggle cannot be empty"
|
||||
},
|
||||
"geoFenceEnabled": {
|
||||
"notNull": "Geo-fence toggle cannot be empty"
|
||||
|
||||
@@ -814,11 +814,11 @@
|
||||
"blockOverseasIp": {
|
||||
"notNull": "Saklar blokir IP luar negeri tidak boleh kosong"
|
||||
},
|
||||
"provinceBlacklistEnabled": {
|
||||
"notNull": "Saklar pemblokiran tingkat provinsi tidak boleh kosong"
|
||||
"regionBlacklistEnabled": {
|
||||
"notNull": "Saklar pemblokiran wilayah tidak boleh kosong"
|
||||
},
|
||||
"cityBlacklistEnabled": {
|
||||
"notNull": "Saklar pemblokiran tingkat kota tidak boleh kosong"
|
||||
"ipv6MatchEnabled": {
|
||||
"notNull": "Saklar pencocokan region IPv6 tidak boleh kosong"
|
||||
},
|
||||
"geoFenceEnabled": {
|
||||
"notNull": "Saklar geo-pagar tidak boleh kosong"
|
||||
|
||||
@@ -807,11 +807,11 @@
|
||||
"blockOverseasIp": {
|
||||
"notNull": "海外 IP ブロック スイッチは必須です"
|
||||
},
|
||||
"provinceBlacklistEnabled": {
|
||||
"notNull": "省レベルの地域遮断スイッチは必須です"
|
||||
"regionBlacklistEnabled": {
|
||||
"notNull": "地域遮断スイッチは必須です"
|
||||
},
|
||||
"cityBlacklistEnabled": {
|
||||
"notNull": "市レベルの地域遮断スイッチは必須です"
|
||||
"ipv6MatchEnabled": {
|
||||
"notNull": "IPv6 地域マッチングスイッチは必須です"
|
||||
},
|
||||
"geoFenceEnabled": {
|
||||
"notNull": "ジオフェンススイッチは必須です"
|
||||
|
||||
@@ -807,11 +807,11 @@
|
||||
"blockOverseasIp": {
|
||||
"notNull": "해외 IP 차단 스위치는 필수입니다"
|
||||
},
|
||||
"provinceBlacklistEnabled": {
|
||||
"notNull": "성 지역 차단 스위치는 필수입니다"
|
||||
"regionBlacklistEnabled": {
|
||||
"notNull": "지역 차단 스위치는 필수입니다"
|
||||
},
|
||||
"cityBlacklistEnabled": {
|
||||
"notNull": "시 지역 차단 스위치는 필수입니다"
|
||||
"ipv6MatchEnabled": {
|
||||
"notNull": "IPv6 지역 매칭 스위치는 필수입니다"
|
||||
},
|
||||
"geoFenceEnabled": {
|
||||
"notNull": "지오펜스 스위치는 필수입니다"
|
||||
|
||||
@@ -814,11 +814,11 @@
|
||||
"blockOverseasIp": {
|
||||
"notNull": "Suis sekatan IP luar negara tidak boleh kosong"
|
||||
},
|
||||
"provinceBlacklistEnabled": {
|
||||
"notNull": "Saklar sekatan tahap wilayah tidak boleh kosong"
|
||||
"regionBlacklistEnabled": {
|
||||
"notNull": "Saklar sekatan wilayah tidak boleh kosong"
|
||||
},
|
||||
"cityBlacklistEnabled": {
|
||||
"notNull": "Saklar sekatan tahap bandar tidak boleh kosong"
|
||||
"ipv6MatchEnabled": {
|
||||
"notNull": "Saklar pemadanan rantau IPv6 tidak boleh kosong"
|
||||
},
|
||||
"geoFenceEnabled": {
|
||||
"notNull": "Saklar pagar geo tidak boleh kosong"
|
||||
|
||||
@@ -814,11 +814,11 @@
|
||||
"blockOverseasIp": {
|
||||
"notNull": "สวิตช์บล็อก IP ต่างประเทศต้องไม่ว่าง"
|
||||
},
|
||||
"provinceBlacklistEnabled": {
|
||||
"notNull": "สวิตช์บล็อกภูมิภาคระดับจังหวัดต้องไม่ว่าง"
|
||||
"regionBlacklistEnabled": {
|
||||
"notNull": "สวิตช์บล็อกภูมิภาคต้องไม่ว่าง"
|
||||
},
|
||||
"cityBlacklistEnabled": {
|
||||
"notNull": "สวิตช์บล็อกภูมิภาคระดับเมืองต้องไม่ว่าง"
|
||||
"ipv6MatchEnabled": {
|
||||
"notNull": "สวิตช์จับคู่ภูมิภาค IPv6 ต้องไม่ว่าง"
|
||||
},
|
||||
"geoFenceEnabled": {
|
||||
"notNull": "สวิตช์รั้วภูมิศาสตร์ต้องไม่ว่าง"
|
||||
|
||||
@@ -814,11 +814,11 @@
|
||||
"blockOverseasIp": {
|
||||
"notNull": "Công tắc chặn IP nước ngoài không được để trống"
|
||||
},
|
||||
"provinceBlacklistEnabled": {
|
||||
"notNull": "Công tắc chặn cấp tỉnh không được để trống"
|
||||
"regionBlacklistEnabled": {
|
||||
"notNull": "Công tắc chặn khu vực không được để trống"
|
||||
},
|
||||
"cityBlacklistEnabled": {
|
||||
"notNull": "Công tắc chặn cấp thành phố không được để trống"
|
||||
"ipv6MatchEnabled": {
|
||||
"notNull": "Công tắc khớp vùng IPv6 không được để trống"
|
||||
},
|
||||
"geoFenceEnabled": {
|
||||
"notNull": "Công tắc hàng rào địa lý không được để trống"
|
||||
|
||||
@@ -811,11 +811,11 @@
|
||||
"blockOverseasIp": {
|
||||
"notNull": "海外 IP 拦截开关不能为空"
|
||||
},
|
||||
"provinceBlacklistEnabled": {
|
||||
"notNull": "省级地区拦截开关不能为空"
|
||||
"regionBlacklistEnabled": {
|
||||
"notNull": "地区拦截开关不能为空"
|
||||
},
|
||||
"cityBlacklistEnabled": {
|
||||
"notNull": "市级地区拦截开关不能为空"
|
||||
"ipv6MatchEnabled": {
|
||||
"notNull": "IPv6 地区匹配开关不能为空"
|
||||
},
|
||||
"geoFenceEnabled": {
|
||||
"notNull": "地理围栏开关不能为空"
|
||||
|
||||
@@ -807,11 +807,11 @@
|
||||
"blockOverseasIp": {
|
||||
"notNull": "海外 IP 攔截開關不能為空"
|
||||
},
|
||||
"provinceBlacklistEnabled": {
|
||||
"notNull": "省級地區攔截開關不能為空"
|
||||
"regionBlacklistEnabled": {
|
||||
"notNull": "地區攔截開關不能為空"
|
||||
},
|
||||
"cityBlacklistEnabled": {
|
||||
"notNull": "市級地區攔截開關不能為空"
|
||||
"ipv6MatchEnabled": {
|
||||
"notNull": "IPv6 地區匹配開關不能為空"
|
||||
},
|
||||
"geoFenceEnabled": {
|
||||
"notNull": "地理圍欄開關不能為空"
|
||||
|
||||
@@ -807,11 +807,11 @@
|
||||
"blockOverseasIp": {
|
||||
"notNull": "海外 IP 攔截開關不能為空"
|
||||
},
|
||||
"provinceBlacklistEnabled": {
|
||||
"notNull": "省級地區攔截開關不能為空"
|
||||
"regionBlacklistEnabled": {
|
||||
"notNull": "地區攔截開關不能為空"
|
||||
},
|
||||
"cityBlacklistEnabled": {
|
||||
"notNull": "市級地區攔截開關不能為空"
|
||||
"ipv6MatchEnabled": {
|
||||
"notNull": "IPv6 地區匹配開關不能為空"
|
||||
},
|
||||
"geoFenceEnabled": {
|
||||
"notNull": "地理圍欄開關不能為空"
|
||||
|
||||
@@ -16,8 +16,7 @@ import lombok.experimental.Accessors;
|
||||
/// - 事后补录: 支付成功后是否补充记录命中(用于事后分析,不阻断资金态)
|
||||
/// - 用户标识拦截级别: 是否对 H5 / 主扫等免用户标识方式强制 OAuth, 见 [PayRiskOpenIdLevelEnum]
|
||||
/// - 海外 IP 拦截: 拦截境外 IP 发起的支付请求(第二层)
|
||||
/// - 省级地区拦截: 根据 IP 归属省份匹配省级黑名单(第二层)
|
||||
/// - 市级地区拦截: 根据 IP 归属城市匹配市级黑名单(第二层, 与省级开关独立, 省命中后不执行)
|
||||
/// - 地区拦截: 根据 IP 归属地匹配省级与市级黑名单, 省级命中后不执行市级检查(第二层)
|
||||
/// - 地理围栏: 围栏功能全局开关 + 全局策略(strict/balanced/loose), 开启后各商户 opt-in 才生效(第三层)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@@ -42,12 +41,29 @@ public class PlatformPaySecurityConfig {
|
||||
/// 海外 IP 拦截(默认关闭, 拦截境外 IP 支付请求)
|
||||
private Boolean blockOverseasIp = Boolean.FALSE;
|
||||
|
||||
/// 省级地区拦截(默认关闭, 开启后根据 IP 归属省份匹配省级黑名单)
|
||||
/// 地区拦截(默认关闭, 开启后根据 IP 归属地匹配省级与市级黑名单; 省级命中后不执行市级检查)
|
||||
///
|
||||
/// 合并原省级/市级两个独立开关, 名单数据仍按 type=province/city 在「支付安全 → 黑名单」中区分维护。
|
||||
private Boolean regionBlacklistEnabled = Boolean.FALSE;
|
||||
|
||||
/// 省级地区拦截(兼容旧版 jsonb 配置迁移, 不再参与逻辑)
|
||||
///
|
||||
/// 由 [PlatformSecurityConfigService] 读取时合并进 [regionBlacklistEnabled] 后清理, 勿再使用。
|
||||
@Deprecated
|
||||
private Boolean provinceBlacklistEnabled = Boolean.FALSE;
|
||||
|
||||
/// 市级地区拦截(默认关闭, 开启后根据 IP 归属城市匹配市级黑名单; 与省级开关独立, 省命中后不执行)
|
||||
/// 市级地区拦截(兼容旧版 jsonb 配置迁移, 不再参与逻辑)
|
||||
///
|
||||
/// 由 [PlatformSecurityConfigService] 读取时合并进 [regionBlacklistEnabled] 后清理, 勿再使用。
|
||||
@Deprecated
|
||||
private Boolean cityBlacklistEnabled = Boolean.FALSE;
|
||||
|
||||
/// IPv6 地区匹配开关(默认关闭, IPv6 离线数据精度有限)
|
||||
///
|
||||
/// 开启后省级/市级/海外/地理围栏四项地域检查会对 IPv6 地址执行地区匹配;
|
||||
/// 关闭时 IPv6 地址在地域检查中直通放行。xdb 开源 IPv6 数据精度有限, 商用数据可提高可靠性。
|
||||
private Boolean ipv6MatchEnabled = Boolean.FALSE;
|
||||
|
||||
/// 地理围栏全局开关(默认关闭, 开启后各商户 mch_risk_config.geoFenceEnabled opt-in 才生效)
|
||||
private Boolean geoFenceEnabled = Boolean.FALSE;
|
||||
|
||||
|
||||
@@ -36,13 +36,13 @@ public class PlatformPaySecurityConfigParam {
|
||||
@NotNull(message = "{validation.field.blockOverseasIp.notNull}")
|
||||
private Boolean blockOverseasIp;
|
||||
|
||||
@Schema(description = "省级地区拦截(默认关闭, 开启后根据 IP 归属省份匹配省级黑名单)")
|
||||
@NotNull(message = "{validation.field.provinceBlacklistEnabled.notNull}")
|
||||
private Boolean provinceBlacklistEnabled;
|
||||
@Schema(description = "地区拦截(默认关闭, 开启后根据 IP 归属地匹配省级与市级黑名单; 省级命中后不执行市级检查)")
|
||||
@NotNull(message = "{validation.field.regionBlacklistEnabled.notNull}")
|
||||
private Boolean regionBlacklistEnabled;
|
||||
|
||||
@Schema(description = "市级地区拦截(默认关闭, 开启后根据 IP 归属城市匹配市级黑名单; 与省级开关独立)")
|
||||
@NotNull(message = "{validation.field.cityBlacklistEnabled.notNull}")
|
||||
private Boolean cityBlacklistEnabled;
|
||||
@Schema(description = "IPv6 地区匹配开关(默认关闭, 开启后地域检查对 IPv6 执行匹配; 离线数据精度有限)")
|
||||
@NotNull(message = "{validation.field.ipv6MatchEnabled.notNull}")
|
||||
private Boolean ipv6MatchEnabled;
|
||||
|
||||
@Schema(description = "地理围栏全局开关(默认关闭, 开启后各商户围栏 opt-in 才生效)")
|
||||
@NotNull(message = "{validation.field.geoFenceEnabled.notNull}")
|
||||
|
||||
@@ -32,11 +32,11 @@ public class PlatformPaySecurityConfigResult extends BaseResult {
|
||||
@Schema(description = "海外 IP 拦截(默认关闭, 拦截境外 IP 支付请求)")
|
||||
private Boolean blockOverseasIp;
|
||||
|
||||
@Schema(description = "省级地区拦截(默认关闭, 开启后根据 IP 归属省份匹配省级黑名单)")
|
||||
private Boolean provinceBlacklistEnabled;
|
||||
@Schema(description = "地区拦截(默认关闭, 开启后根据 IP 归属地匹配省级与市级黑名单; 省级命中后不执行市级检查)")
|
||||
private Boolean regionBlacklistEnabled;
|
||||
|
||||
@Schema(description = "市级地区拦截(默认关闭, 开启后根据 IP 归属城市匹配市级黑名单; 与省级开关独立)")
|
||||
private Boolean cityBlacklistEnabled;
|
||||
@Schema(description = "IPv6 地区匹配开关(默认关闭, 开启后地域检查对 IPv6 执行匹配; 离线数据精度有限)")
|
||||
private Boolean ipv6MatchEnabled;
|
||||
|
||||
@Schema(description = "地理围栏全局开关(默认关闭, 开启后各商户围栏 opt-in 才生效)")
|
||||
private Boolean geoFenceEnabled;
|
||||
|
||||
@@ -262,7 +262,16 @@ public class PlatformSecurityConfigService {
|
||||
return defaultPaySecurityConfig();
|
||||
}
|
||||
PlatformPaySecurityConfig config = JacksonUtil.toBean(json, PlatformPaySecurityConfig.class);
|
||||
return config == null ? defaultPaySecurityConfig() : config;
|
||||
if (config == null) {
|
||||
return defaultPaySecurityConfig();
|
||||
}
|
||||
// 兼容旧版: 原省级/市级开关已合并为地区拦截, 旧字段任一开启则迁移为地区拦截开启
|
||||
if (config.getRegionBlacklistEnabled() == null) {
|
||||
boolean legacyOn = Boolean.TRUE.equals(config.getProvinceBlacklistEnabled())
|
||||
|| Boolean.TRUE.equals(config.getCityBlacklistEnabled());
|
||||
config.setRegionBlacklistEnabled(legacyOn);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/// 支付安全配置默认值: 默认开启风控、命中阻断、事后补录、增强拦截
|
||||
@@ -273,6 +282,7 @@ public class PlatformSecurityConfigService {
|
||||
.setRiskCheckAfterPay(true)
|
||||
.setRiskOpenIdLevel(PayRiskOpenIdLevelEnum.ENHANCED.getCode())
|
||||
.setBlockOverseasIp(false)
|
||||
.setIpv6MatchEnabled(false)
|
||||
.setGeoFenceStrategy(GEO_FENCE_STRATEGY_BALANCED);
|
||||
}
|
||||
|
||||
@@ -293,6 +303,9 @@ public class PlatformSecurityConfigService {
|
||||
PlatformPaySecurityConfig.class,
|
||||
defaultPaySecurityConfig());
|
||||
PlatformSecurityConfigConvert.CONVERT.copy(param, data);
|
||||
// 清理旧版省级/市级开关字段(已合并进 regionBlacklistEnabled, jsonb 不再保留)
|
||||
data.setProvinceBlacklistEnabled(null);
|
||||
data.setCityBlacklistEnabled(null);
|
||||
systemConfigService.updateConfig(PlatformConfigTypeEnum.PAY_SECURITY, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,12 +79,8 @@ public class DefaultPayRiskChecker implements PayRiskChecker {
|
||||
if (Boolean.TRUE.equals(ctx.getBlockOverseasIp())) {
|
||||
checkOverseasIp(ctx, throwOnHit);
|
||||
}
|
||||
// IP 省级地区黑名单(第二层全局地区名单, 受 provinceBlacklistEnabled 开关控制; 省命中后不再执行市级检查)
|
||||
boolean provinceHit = checkProvinceBlacklist(ctx, throwOnHit);
|
||||
// IP 市级地区黑名单(第二层, 受 cityBlacklistEnabled 开关控制, 与省级开关独立)
|
||||
if (!provinceHit && Boolean.TRUE.equals(ctx.getCityBlacklistEnabled())) {
|
||||
checkCityBlacklist(ctx, throwOnHit);
|
||||
}
|
||||
// IP 地区黑名单(第二层全局地区名单, 受 regionBlacklistEnabled 开关控制; 省级命中后不执行市级检查)
|
||||
checkRegionBlacklist(ctx, throwOnHit);
|
||||
// L3 地理围栏(门店市级: IP 归属城市与门店城市比对)
|
||||
if (Boolean.TRUE.equals(ctx.getGeoFenceEnabled())) {
|
||||
checkStoreGeoFence(ctx, throwOnHit);
|
||||
@@ -111,11 +107,8 @@ public class DefaultPayRiskChecker implements PayRiskChecker {
|
||||
if (Boolean.TRUE.equals(ctx.getBlockOverseasIp())) {
|
||||
checkOverseasIp(ctx, false);
|
||||
}
|
||||
// IP 地区黑名单(事后补录, 受 provinceBlacklistEnabled 开关控制; 省命中后不再补录市级)
|
||||
boolean provinceHit = checkProvinceBlacklist(ctx, false);
|
||||
if (!provinceHit && Boolean.TRUE.equals(ctx.getCityBlacklistEnabled())) {
|
||||
checkCityBlacklist(ctx, false);
|
||||
}
|
||||
// IP 地区黑名单(事后补录, 受 regionBlacklistEnabled 开关控制; 省级命中后不补录市级)
|
||||
checkRegionBlacklist(ctx, false);
|
||||
// L3 地理围栏(事后补录: IP 归属城市与门店城市比对)
|
||||
if (Boolean.TRUE.equals(ctx.getGeoFenceEnabled())) {
|
||||
checkStoreGeoFence(ctx, false);
|
||||
@@ -173,70 +166,53 @@ public class DefaultPayRiskChecker implements PayRiskChecker {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// IP 省级地区黑名单(第二层全局地区名单)
|
||||
/// IP 地区黑名单(第二层全局地区名单, 含省级 + 市级)
|
||||
///
|
||||
/// IP 归属省份在省级黑名单中则命中, value 存省行政区划编码(2位, 如"44")。
|
||||
/// 需平台配置 provinceBlacklistEnabled=true 才执行; IPv6 / 内网 / 解析失败 / 无法映射编码 → fail-open 放行。
|
||||
/// @return 是否命中(用于外层短路市级检查)
|
||||
private boolean checkProvinceBlacklist(PayRiskCheckContext ctx, boolean throwOnHit) {
|
||||
// 省级拦截开关关闭时跳过
|
||||
if (!Boolean.TRUE.equals(ctx.getProvinceBlacklistEnabled())) {
|
||||
/// 受 regionBlacklistEnabled 开关控制。先按 IP 归属省份匹配省级黑名单(value 存省行政区划编码, 如"44"),
|
||||
/// 命中即拦截不再执行市级检查; 未命中再按 IP 归属城市匹配市级黑名单(value 存市行政区划编码, 如"4403";
|
||||
/// 直辖市无独立市级, 城市名单存省编码, 由 [RegionCodeResolver#resolveCityCode] 回落为省编码)。
|
||||
/// 省/市名单数据在「支付安全 → 黑名单」中按 type 区分维护。
|
||||
/// IPv6(受 ipv6MatchEnabled 控制) / 内网 / 解析失败 / 无法映射编码 → fail-open 放行。
|
||||
/// @return 是否命中(省级或市级)
|
||||
private boolean checkRegionBlacklist(PayRiskCheckContext ctx, boolean throwOnHit) {
|
||||
// 地区拦截开关关闭时跳过
|
||||
if (!Boolean.TRUE.equals(ctx.getRegionBlacklistEnabled())) {
|
||||
return false;
|
||||
}
|
||||
String ip = ctx.getClientIp();
|
||||
if (StrUtil.isBlank(ip)) {
|
||||
return false;
|
||||
}
|
||||
// IPv6 暂不参与地区判定
|
||||
// IPv6 受 ipv6MatchEnabled 开关控制(默认关闭, 离线数据精度有限)
|
||||
if (Validator.isIpv6(ip)) {
|
||||
return false;
|
||||
}
|
||||
// 内网/回环地址直通放行
|
||||
if (NetUtil.isInnerIP(ip)) {
|
||||
return false;
|
||||
if (!Boolean.TRUE.equals(ctx.getIpv6MatchEnabled())) {
|
||||
return false;
|
||||
}
|
||||
// 开关开启: 跳过 NetUtil.isInnerIP(仅支持 IPv4), IPv6 内网由 xdb 查询 fail-open 兜底
|
||||
} else {
|
||||
// IPv4 内网/回环地址直通放行
|
||||
if (NetUtil.isInnerIP(ip)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
IpRegion region = ipToRegionService.getRegionByIp(ip);
|
||||
// 解析失败 → fail-open
|
||||
if (region == null) {
|
||||
return false;
|
||||
}
|
||||
// IP 归属省份 → 行政区划编码(港澳台/"0"段/未知无法映射 → fail-open)
|
||||
// 省级名单: IP 归属省份 → 行政区划编码(港澳台/"0"段/未知无法映射 → 跳过省级, 继续市级)
|
||||
String provinceCode = regionCodeResolver.resolveProvinceCode(region.getProvince());
|
||||
if (StrUtil.isBlank(provinceCode)) {
|
||||
return false;
|
||||
if (StrUtil.isNotBlank(provinceCode)
|
||||
&& rejectIfBlocked(ctx, PayBlacklistTypeEnum.PROVINCE.getCode(), provinceCode, null, throwOnHit)) {
|
||||
// 省命中即拦截, 不再执行市级检查
|
||||
return true;
|
||||
}
|
||||
return rejectIfBlocked(ctx, PayBlacklistTypeEnum.PROVINCE.getCode(), provinceCode, null, throwOnHit);
|
||||
}
|
||||
|
||||
/// IP 市级地区黑名单(第二层全局地区名单)
|
||||
///
|
||||
/// IP 归属城市在市级黑名单中则命中, value 存市行政区划编码(4位, 如"4403")。
|
||||
/// 直辖市无独立市级(城市名单存省编码), 由 [RegionCodeResolver#resolveCityCode] 回落为省编码。
|
||||
/// 需平台配置 cityBlacklistEnabled=true 才执行; IPv6 / 内网 / 解析失败 / 无法映射编码 → fail-open 放行。
|
||||
private void checkCityBlacklist(PayRiskCheckContext ctx, boolean throwOnHit) {
|
||||
String ip = ctx.getClientIp();
|
||||
if (StrUtil.isBlank(ip)) {
|
||||
return;
|
||||
}
|
||||
// IPv6 暂不参与地区判定
|
||||
if (Validator.isIpv6(ip)) {
|
||||
return;
|
||||
}
|
||||
// 内网/回环地址直通放行
|
||||
if (NetUtil.isInnerIP(ip)) {
|
||||
return;
|
||||
}
|
||||
IpRegion region = ipToRegionService.getRegionByIp(ip);
|
||||
// 解析失败 → fail-open
|
||||
if (region == null) {
|
||||
return;
|
||||
}
|
||||
// IP 归属城市 → 行政区划编码(直辖市回落省编码; "0"段/未知无法映射 → fail-open)
|
||||
// 市级名单: IP 归属城市 → 行政区划编码(直辖市回落省编码; "0"段/未知无法映射 → fail-open)
|
||||
String cityCode = regionCodeResolver.resolveCityCode(region.getProvince(), region.getCity());
|
||||
if (StrUtil.isBlank(cityCode)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
rejectIfBlocked(ctx, PayBlacklistTypeEnum.CITY.getCode(), cityCode, null, throwOnHit);
|
||||
return rejectIfBlocked(ctx, PayBlacklistTypeEnum.CITY.getCode(), cityCode, null, throwOnHit);
|
||||
}
|
||||
|
||||
/// 海外 IP 地域拦截(country≠中国, 港澳台放行; 未知/内网 fail-open)
|
||||
@@ -249,13 +225,17 @@ public class DefaultPayRiskChecker implements PayRiskChecker {
|
||||
if (StrUtil.isBlank(ip)) {
|
||||
return;
|
||||
}
|
||||
// IPv6 暂不参与网段判定, 直接放行(NetUtil.isInnerIP 仅支持 IPv4, 传入 IPv6 会抛异常)
|
||||
// IPv6 受 ipv6MatchEnabled 开关控制(默认关闭, 离线数据精度有限)
|
||||
if (Validator.isIpv6(ip)) {
|
||||
return;
|
||||
}
|
||||
// 内网/回环地址直通放行(网络层判定, 不查库)
|
||||
if (NetUtil.isInnerIP(ip)) {
|
||||
return;
|
||||
if (!Boolean.TRUE.equals(ctx.getIpv6MatchEnabled())) {
|
||||
return;
|
||||
}
|
||||
// 开关开启: 跳过 NetUtil.isInnerIP(仅支持 IPv4, 传 IPv6 会抛异常), IPv6 内网由 xdb fail-open 兜底
|
||||
} else {
|
||||
// IPv4 内网/回环地址直通放行(网络层判定, 不查库)
|
||||
if (NetUtil.isInnerIP(ip)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
IpRegion region = ipToRegionService.getRegionByIp(ip);
|
||||
// 未知(IPv6/查询失败)/国内(含港澳台) → 放行
|
||||
@@ -298,13 +278,17 @@ public class DefaultPayRiskChecker implements PayRiskChecker {
|
||||
if (StrUtil.isBlank(ip)) {
|
||||
return;
|
||||
}
|
||||
// IPv6 暂不参与地区判定
|
||||
// IPv6 受 ipv6MatchEnabled 开关控制(默认关闭, 离线数据精度有限)
|
||||
if (Validator.isIpv6(ip)) {
|
||||
return;
|
||||
}
|
||||
// 内网/回环地址直通放行
|
||||
if (NetUtil.isInnerIP(ip)) {
|
||||
return;
|
||||
if (!Boolean.TRUE.equals(ctx.getIpv6MatchEnabled())) {
|
||||
return;
|
||||
}
|
||||
// 开关开启: 跳过 NetUtil.isInnerIP(仅支持 IPv4), IPv6 内网由 xdb 查询 fail-open 兜底
|
||||
} else {
|
||||
// IPv4 内网/回环地址直通放行
|
||||
if (NetUtil.isInnerIP(ip)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
IpRegion region = ipToRegionService.getRegionByIp(ip);
|
||||
if (region == null) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.daxpay.open.plugin.risk.strategy;
|
||||
|
||||
import cn.daxpay.open.payment.strategy.risk.PayRiskCheckContext;
|
||||
import cn.daxpay.open.payment.strategy.risk.RegionCodeResolver;
|
||||
import cn.daxpay.open.platform.capability.audit.log.service.ip2region.IpRegion;
|
||||
import cn.daxpay.open.platform.capability.audit.log.service.ip2region.IpToRegionService;
|
||||
import cn.daxpay.open.platform.core.enums.pay.channel.ChannelEnum;
|
||||
@@ -58,6 +59,7 @@ class DefaultPayRiskCheckerTest {
|
||||
private static final String WECHAT_OPENID_TYPE = PayBlacklistTypeEnum.WECHAT_OPENID.getCode();
|
||||
private static final String OVERSEAS_IP_TYPE = PayBlacklistTypeEnum.OVERSEAS_IP.getCode();
|
||||
private static final String PROVINCE_TYPE = PayBlacklistTypeEnum.PROVINCE.getCode();
|
||||
private static final String CITY_TYPE = PayBlacklistTypeEnum.CITY.getCode();
|
||||
private static final String BLACKLIST_MSG_KEY = "pay.error.risk.blacklist";
|
||||
|
||||
@Mock
|
||||
@@ -66,12 +68,14 @@ class DefaultPayRiskCheckerTest {
|
||||
private PayRiskHitService payRiskHitService;
|
||||
@Mock
|
||||
private IpToRegionService ipToRegionService;
|
||||
@Mock
|
||||
private RegionCodeResolver regionCodeResolver;
|
||||
|
||||
private DefaultPayRiskChecker checker;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
checker = new DefaultPayRiskChecker(payBlacklistService, payRiskHitService, ipToRegionService);
|
||||
checker = new DefaultPayRiskChecker(payBlacklistService, payRiskHitService, ipToRegionService, regionCodeResolver);
|
||||
// 名单默认不命中, 让流程走到海外检查; 个别用例用 eq() 精确覆写为命中
|
||||
lenient().when(payBlacklistService.findActive(anyString(), anyString(), any()))
|
||||
.thenReturn(Optional.empty());
|
||||
@@ -130,7 +134,7 @@ class DefaultPayRiskCheckerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("海外检查: IPv6 回环直通放行(NetUtil.isInnerIP 仅支持 IPv4, 传入 IPv6 会抛异常)")
|
||||
@DisplayName("海外检查: IPv6 回环直通放行(ipv6MatchEnabled 默认关闭)")
|
||||
void overseasIp_ipv6Loopback_shouldPassThroughWithoutLookup() {
|
||||
assertDoesNotThrow(() -> checker.checkBeforePay(overseasCtx("::1")));
|
||||
|
||||
@@ -139,7 +143,7 @@ class DefaultPayRiskCheckerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("海外检查: IPv6 公网地址直通放行")
|
||||
@DisplayName("海外检查: IPv6 公网地址直通放行(ipv6MatchEnabled 默认关闭)")
|
||||
void overseasIp_ipv6Public_shouldPassThroughWithoutLookup() {
|
||||
assertDoesNotThrow(() -> checker.checkBeforePay(overseasCtx("2409:8a20:210:0:0:0:0:1")));
|
||||
|
||||
@@ -147,6 +151,35 @@ class DefaultPayRiskCheckerTest {
|
||||
verifyNoInteractions(payRiskHitService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("海外检查: ipv6MatchEnabled 开启时 IPv6 海外地址命中拦截")
|
||||
void overseasIp_ipv6MatchEnabled_shouldLookupAndBlock() {
|
||||
// 开关开启: IPv6 走 getRegionByIp 查询
|
||||
IpRegion overseas = new IpRegion();
|
||||
overseas.setCountry("United States");
|
||||
when(ipToRegionService.getRegionByIp("2606:4700:4700::1111")).thenReturn(overseas);
|
||||
|
||||
assertThrows(BizInfoException.class,
|
||||
() -> checker.checkBeforePay(overseasCtx("2606:4700:4700::1111").setIpv6MatchEnabled(true)));
|
||||
|
||||
verify(ipToRegionService).getRegionByIp("2606:4700:4700::1111");
|
||||
verify(payRiskHitService).recordHit(any(), eq(OVERSEAS_IP_TYPE), eq("2606:4700:4700::1111"), eq(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("海外检查: ipv6MatchEnabled 开启时 IPv6 国内地址放行")
|
||||
void overseasIp_ipv6MatchEnabled_chinaIpShouldPass() {
|
||||
// 开关开启: IPv6 走查询, 结果为中国 → 放行
|
||||
IpRegion china = new IpRegion();
|
||||
china.setCountry("中国");
|
||||
when(ipToRegionService.getRegionByIp("240e:3b7::1")).thenReturn(china);
|
||||
|
||||
assertDoesNotThrow(() -> checker.checkBeforePay(overseasCtx("240e:3b7::1").setIpv6MatchEnabled(true)));
|
||||
|
||||
verify(ipToRegionService).getRegionByIp("240e:3b7::1");
|
||||
verify(payRiskHitService, never()).recordHit(any(), eq(OVERSEAS_IP_TYPE), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("海外检查: xdb 查询失败(region=null)放行不记录")
|
||||
void overseasIp_lookupReturnsNull_shouldPass() {
|
||||
@@ -278,7 +311,7 @@ class DefaultPayRiskCheckerTest {
|
||||
.setOpenId("uid-1")
|
||||
.setBlockOverseasIp(false)
|
||||
.setBlacklistEnabled(null)
|
||||
.setProvinceBlacklistEnabled(false)
|
||||
.setRegionBlacklistEnabled(false)
|
||||
.setBlockOnHit(true);
|
||||
|
||||
assertDoesNotThrow(() -> checker.checkBeforePay(ctx));
|
||||
@@ -441,32 +474,34 @@ class DefaultPayRiskCheckerTest {
|
||||
verify(payRiskHitService, never()).recordHit(any(), eq(ALIPAY_USER_TYPE), eq("uid-a"), any());
|
||||
}
|
||||
|
||||
// ==================== 省级地区黑名单 ====================
|
||||
// ==================== 地区黑名单(省级 + 市级, 合并开关) ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("省级名单: provinceBlacklistEnabled 关闭(null/false)时不触发检查")
|
||||
void provinceBlacklist_switchOff_shouldNotCheck() {
|
||||
@DisplayName("地区拦截: regionBlacklistEnabled 关闭(null/false)时不触发检查")
|
||||
void regionBlacklist_switchOff_shouldNotCheck() {
|
||||
when(ipToRegionService.getRegionByIp("114.114.114.114"))
|
||||
.thenReturn(regionWithProvince("中国", "广东省", "电信"));
|
||||
.thenReturn(regionWithCity("中国", "广东省", "深圳市", "电信"));
|
||||
PayRiskCheckContext ctx = new PayRiskCheckContext()
|
||||
.setClientIp("114.114.114.114")
|
||||
.setBlockOverseasIp(false)
|
||||
.setProvinceBlacklistEnabled(null)
|
||||
.setRegionBlacklistEnabled(null)
|
||||
.setBlockOnHit(true);
|
||||
|
||||
assertDoesNotThrow(() -> checker.checkBeforePay(ctx));
|
||||
|
||||
// 开关关闭 → 不调 ip2region, 不查省级名单
|
||||
// 开关关闭 → 不调 ip2region, 不查省/市级名单
|
||||
verify(ipToRegionService, never()).getRegionByIp(anyString());
|
||||
verify(payBlacklistService, never()).findActive(eq(PROVINCE_TYPE), anyString(), any());
|
||||
verify(payBlacklistService, never()).findActive(eq(CITY_TYPE), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("省级名单: IP 归属省在黑名单中命中阻断")
|
||||
void provinceBlacklist_hit_shouldThrow() {
|
||||
@DisplayName("地区拦截: IP 归属省在省级名单中命中阻断")
|
||||
void regionBlacklist_provinceHit_shouldThrow() {
|
||||
when(ipToRegionService.getRegionByIp("114.114.114.114"))
|
||||
.thenReturn(regionWithProvince("中国", "广东省", "电信"));
|
||||
when(payBlacklistService.findActive(eq(PROVINCE_TYPE), eq("广东省"), eq(null)))
|
||||
.thenReturn(regionWithCity("中国", "广东省", "深圳市", "电信"));
|
||||
when(regionCodeResolver.resolveProvinceCode("广东省")).thenReturn("44");
|
||||
when(payBlacklistService.findActive(eq(PROVINCE_TYPE), eq("44"), eq(null)))
|
||||
.thenReturn(Optional.of(bl(55L)));
|
||||
PayRiskCheckContext ctx = noOverseasCtx().setClientIp("114.114.114.114");
|
||||
|
||||
@@ -474,57 +509,99 @@ class DefaultPayRiskCheckerTest {
|
||||
() -> checker.checkBeforePay(ctx));
|
||||
|
||||
assertEquals(BLACKLIST_MSG_KEY, ex.getMessageKey());
|
||||
verify(payRiskHitService).recordHit(any(), eq(PROVINCE_TYPE), eq("广东省"), eq(55L));
|
||||
verify(payRiskHitService).recordHit(any(), eq(PROVINCE_TYPE), eq("44"), eq(55L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("省级名单: IP 归属省不在黑名单中放行")
|
||||
void provinceBlacklist_miss_shouldPass() {
|
||||
@DisplayName("地区拦截: 省级名单命中后短路市级检查")
|
||||
void regionBlacklist_provinceHit_shouldShortCircuitCity() {
|
||||
when(ipToRegionService.getRegionByIp("114.114.114.114"))
|
||||
.thenReturn(regionWithProvince("中国", "广东省", "电信"));
|
||||
.thenReturn(regionWithCity("中国", "广东省", "深圳市", "电信"));
|
||||
when(regionCodeResolver.resolveProvinceCode("广东省")).thenReturn("44");
|
||||
when(payBlacklistService.findActive(eq(PROVINCE_TYPE), eq("44"), eq(null)))
|
||||
.thenReturn(Optional.of(bl(55L)));
|
||||
PayRiskCheckContext ctx = noOverseasCtx().setClientIp("114.114.114.114");
|
||||
|
||||
assertThrows(BizInfoException.class, () -> checker.checkBeforePay(ctx));
|
||||
|
||||
// 省命中 → 不查市级名单
|
||||
verify(payBlacklistService).findActive(eq(PROVINCE_TYPE), eq("44"), eq(null));
|
||||
verify(payBlacklistService, never()).findActive(eq(CITY_TYPE), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("地区拦截: 省级未命中、市级命中时阻断")
|
||||
void regionBlacklist_cityHit_shouldThrow() {
|
||||
when(ipToRegionService.getRegionByIp("114.114.114.114"))
|
||||
.thenReturn(regionWithCity("中国", "广东省", "深圳市", "电信"));
|
||||
when(regionCodeResolver.resolveProvinceCode("广东省")).thenReturn("44");
|
||||
when(regionCodeResolver.resolveCityCode("广东省", "深圳市")).thenReturn("4403");
|
||||
when(payBlacklistService.findActive(eq(CITY_TYPE), eq("4403"), eq(null)))
|
||||
.thenReturn(Optional.of(bl(66L)));
|
||||
PayRiskCheckContext ctx = noOverseasCtx().setClientIp("114.114.114.114");
|
||||
|
||||
BizInfoException ex = assertThrows(BizInfoException.class,
|
||||
() -> checker.checkBeforePay(ctx));
|
||||
|
||||
assertEquals(BLACKLIST_MSG_KEY, ex.getMessageKey());
|
||||
verify(payRiskHitService).recordHit(any(), eq(CITY_TYPE), eq("4403"), eq(66L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("地区拦截: 省/市名单均未命中放行")
|
||||
void regionBlacklist_miss_shouldPass() {
|
||||
when(ipToRegionService.getRegionByIp("114.114.114.114"))
|
||||
.thenReturn(regionWithCity("中国", "广东省", "深圳市", "电信"));
|
||||
when(regionCodeResolver.resolveProvinceCode("广东省")).thenReturn("44");
|
||||
when(regionCodeResolver.resolveCityCode("广东省", "深圳市")).thenReturn("4403");
|
||||
PayRiskCheckContext ctx = noOverseasCtx().setClientIp("114.114.114.114");
|
||||
|
||||
assertDoesNotThrow(() -> checker.checkBeforePay(ctx));
|
||||
|
||||
verify(payBlacklistService).findActive(eq(PROVINCE_TYPE), eq("广东省"), eq(null));
|
||||
verify(payRiskHitService, never()).recordHit(any(), eq(PROVINCE_TYPE), anyString(), any());
|
||||
// 省级查一次未命中, 继续查市级
|
||||
verify(payBlacklistService).findActive(eq(PROVINCE_TYPE), eq("44"), eq(null));
|
||||
verify(payBlacklistService).findActive(eq(CITY_TYPE), eq("4403"), eq(null));
|
||||
verify(payRiskHitService, never()).recordHit(any(), anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("省级名单: 内网 IP 不查省份")
|
||||
void provinceBlacklist_innerIp_shouldSkip() {
|
||||
@DisplayName("地区拦截: 内网 IP 不查归属地")
|
||||
void regionBlacklist_innerIp_shouldSkip() {
|
||||
PayRiskCheckContext ctx = noOverseasCtx().setClientIp("192.168.1.1");
|
||||
|
||||
assertDoesNotThrow(() -> checker.checkBeforePay(ctx));
|
||||
|
||||
verify(ipToRegionService, never()).getRegionByIp(anyString());
|
||||
verify(payBlacklistService, never()).findActive(eq(PROVINCE_TYPE), anyString(), any());
|
||||
verify(payBlacklistService, never()).findActive(eq(CITY_TYPE), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("省级名单: ip2region 返回 null fail-open")
|
||||
void provinceBlacklist_nullRegion_shouldFailOpen() {
|
||||
@DisplayName("地区拦截: ip2region 返回 null fail-open")
|
||||
void regionBlacklist_nullRegion_shouldFailOpen() {
|
||||
when(ipToRegionService.getRegionByIp("114.114.114.114")).thenReturn(null);
|
||||
PayRiskCheckContext ctx = noOverseasCtx().setClientIp("114.114.114.114");
|
||||
|
||||
assertDoesNotThrow(() -> checker.checkBeforePay(ctx));
|
||||
|
||||
verify(payBlacklistService, never()).findActive(eq(PROVINCE_TYPE), anyString(), any());
|
||||
verify(payBlacklistService, never()).findActive(eq(CITY_TYPE), anyString(), any());
|
||||
verify(payRiskHitService, never()).recordHit(any(), eq(PROVINCE_TYPE), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("省级名单: checkAfterPay 事后补录仅记录不阻断")
|
||||
void provinceBlacklist_afterPay_shouldRecordOnly() {
|
||||
@DisplayName("地区拦截: checkAfterPay 事后补录仅记录不阻断")
|
||||
void regionBlacklist_afterPay_shouldRecordOnly() {
|
||||
when(ipToRegionService.getRegionByIp("114.114.114.114"))
|
||||
.thenReturn(regionWithProvince("中国", "广东省", "电信"));
|
||||
when(payBlacklistService.findActive(eq(PROVINCE_TYPE), eq("广东省"), eq(null)))
|
||||
.thenReturn(regionWithCity("中国", "广东省", "深圳市", "电信"));
|
||||
when(regionCodeResolver.resolveProvinceCode("广东省")).thenReturn("44");
|
||||
when(payBlacklistService.findActive(eq(PROVINCE_TYPE), eq("44"), eq(null)))
|
||||
.thenReturn(Optional.of(bl(55L)));
|
||||
PayRiskCheckContext ctx = noOverseasCtx().setClientIp("114.114.114.114");
|
||||
|
||||
assertDoesNotThrow(() -> checker.checkAfterPay(ctx));
|
||||
|
||||
verify(payRiskHitService).recordHit(any(), eq(PROVINCE_TYPE), eq("广东省"), eq(55L));
|
||||
verify(payRiskHitService).recordHit(any(), eq(PROVINCE_TYPE), eq("44"), eq(55L));
|
||||
}
|
||||
|
||||
// ==================== hasOpenIdBlacklist 缓存 ====================
|
||||
@@ -567,7 +644,7 @@ class DefaultPayRiskCheckerTest {
|
||||
return new PayRiskCheckContext()
|
||||
.setBlockOverseasIp(false)
|
||||
.setBlacklistEnabled(true)
|
||||
.setProvinceBlacklistEnabled(true)
|
||||
.setRegionBlacklistEnabled(true)
|
||||
.setBlockOnHit(true);
|
||||
}
|
||||
|
||||
@@ -584,8 +661,8 @@ class DefaultPayRiskCheckerTest {
|
||||
return new IpRegion().setCountry(country).setIsp(isp);
|
||||
}
|
||||
|
||||
/** 构造含 province 的 IpRegion(省级名单测试用) */
|
||||
private static IpRegion regionWithProvince(String country, String province, String isp) {
|
||||
return new IpRegion().setCountry(country).setProvince(province).setIsp(isp);
|
||||
/** 构造含 province/city 的 IpRegion(地区名单测试用) */
|
||||
private static IpRegion regionWithCity(String country, String province, String city, String isp) {
|
||||
return new IpRegion().setCountry(country).setProvince(province).setCity(city).setIsp(isp);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user