fix: map Pakistan aliases for MifaPay web payments

This commit is contained in:
z 2026-05-03 00:19:54 +08:00
parent c7a650ce7f
commit 399da096b0
7 changed files with 275 additions and 199 deletions

View File

@ -6,8 +6,10 @@ import com.red.circle.tool.core.text.StringUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -17,6 +19,16 @@ public final class CountryCodeAliasUtils {
private static final String SA = "SA"; private static final String SA = "SA";
private static final String KSA = "KSA"; private static final String KSA = "KSA";
private static final String PK = "PK";
private static final String PAK = "PAK";
private static final Map<String, Set<String>> COUNTRY_CODE_ALIAS_GROUPS;
static {
Map<String, Set<String>> aliasGroups = new LinkedHashMap<>();
aliasGroups.put(SA, Set.of(SA, KSA));
aliasGroups.put(PK, Set.of(PK, PAK, "P2", "P3"));
COUNTRY_CODE_ALIAS_GROUPS = Collections.unmodifiableMap(aliasGroups);
}
private CountryCodeAliasUtils() { private CountryCodeAliasUtils() {
} }
@ -36,6 +48,22 @@ public final class CountryCodeAliasUtils {
return isSaudiCode(normalized) ? SA : normalized; return isSaudiCode(normalized) ? SA : normalized;
} }
/**
* 支付国家码归一化三方不识别的业务别名会转换为真实国家码.
*/
public static String normalizePaymentCountryCode(String code) {
String normalized = normalize(code);
if (StringUtils.isBlank(normalized)) {
return null;
}
return COUNTRY_CODE_ALIAS_GROUPS.entrySet().stream()
.filter(entry -> entry.getValue().contains(normalized))
.map(Map.Entry::getKey)
.findFirst()
.orElse(normalized);
}
/** /**
* 是否是沙特国家码. * 是否是沙特国家码.
*/ */
@ -90,9 +118,9 @@ public final class CountryCodeAliasUtils {
aliasCodes.forEach(code -> appendCode(result, code)); aliasCodes.forEach(code -> appendCode(result, code));
} }
if (result.stream().anyMatch(CountryCodeAliasUtils::isSaudiCode)) { LinkedHashSet<String> originalCodes = new LinkedHashSet<>(result);
result.add(SA); for (String code : originalCodes) {
result.add(KSA); result.addAll(aliasGroupCodes(code));
} }
return result; return result;
} }
@ -116,4 +144,12 @@ public final class CountryCodeAliasUtils {
result.add(normalized); result.add(normalized);
} }
} }
private static Set<String> aliasGroupCodes(String code) {
String normalized = normalizePaymentCountryCode(code);
if (StringUtils.isBlank(normalized)) {
return Collections.emptySet();
}
return COUNTRY_CODE_ALIAS_GROUPS.getOrDefault(normalized, Set.of(normalized));
}
} }

View File

@ -0,0 +1,123 @@
package com.red.circle.order.app.command.pay.web;
import com.red.circle.common.business.core.util.CountryCodeAliasUtils;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.order.infra.database.rds.entity.pay.PayCountry;
import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelDetailsService;
import com.red.circle.order.infra.database.rds.service.pay.PayCountryService;
import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient;
import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 支付国家兼容映射.
*/
@Component
@RequiredArgsConstructor
public class PayCountryAliasResolver {
private final PayCountryService payCountryService;
private final SysCountryCodeClient sysCountryCodeClient;
private final PayCountryChannelDetailsService payCountryChannelDetailsService;
public PayCountry resolveUsablePayCountry(Long payCountryOrCountryId) {
if (Objects.isNull(payCountryOrCountryId)) {
return payCountryService.getShelfSortMax();
}
PayCountry payCountry = payCountryService.getById(payCountryOrCountryId);
if (isUsablePayCountry(payCountry)) {
return payCountry;
}
Long countryId = Objects.nonNull(payCountry) ? payCountry.getCountryId() : payCountryOrCountryId;
PayCountry compatiblePayCountry = resolveUsablePayCountryByCountryId(countryId);
return Objects.nonNull(compatiblePayCountry) ? compatiblePayCountry : payCountry;
}
public PayCountry resolveUsablePayCountryByCountryId(Long countryId) {
PayCountry payCountry = getUsablePayCountryByCountryId(countryId);
if (Objects.nonNull(payCountry)) {
return payCountry;
}
return getMappedUsablePayCountryByCountryId(countryId);
}
private PayCountry getMappedUsablePayCountryByCountryId(Long countryId) {
SysCountryCodeDTO countryCode = getCountryCodeById(countryId);
if (Objects.isNull(countryCode)) {
return null;
}
for (String candidateCode : getMappedCountryCodes(countryCode)) {
SysCountryCodeDTO mappedCountry = ResponseAssert.requiredSuccess(
sysCountryCodeClient.getByCode(candidateCode));
if (Objects.isNull(mappedCountry)
|| Objects.equals(mappedCountry.getId(), countryCode.getId())) {
continue;
}
PayCountry mappedPayCountry = getUsablePayCountryByCountryId(mappedCountry.getId());
if (Objects.nonNull(mappedPayCountry)) {
return mappedPayCountry;
}
}
return null;
}
private Set<String> getMappedCountryCodes(SysCountryCodeDTO countryCode) {
LinkedHashSet<String> result = new LinkedHashSet<>();
// 后续新增同国别名时可在别名国家的 countryCodeAliases 配置真实国家码.
if (CollectionUtils.isNotEmpty(countryCode.getCountryCodeAliases())) {
countryCode.getCountryCodeAliases().stream()
.map(CountryCodeAliasUtils::normalizePaymentCountryCode)
.filter(StringUtils::isNotBlank)
.forEach(result::add);
}
String defaultCode = CountryCodeAliasUtils.normalizePaymentCountryCode(
StringUtils.isNotBlank(countryCode.getAlphaTwo())
? countryCode.getAlphaTwo()
: countryCode.getAlphaThree());
if (StringUtils.isNotBlank(defaultCode)) {
result.add(defaultCode);
}
return result;
}
private SysCountryCodeDTO getCountryCodeById(Long countryId) {
if (Objects.isNull(countryId)) {
return null;
}
return ResponseAssert.requiredSuccess(sysCountryCodeClient.getById(countryId));
}
private PayCountry getUsablePayCountryByCountryId(Long countryId) {
if (Objects.isNull(countryId)) {
return null;
}
return payCountryService.query()
.eq(PayCountry::getShelf, Boolean.TRUE)
.eq(PayCountry::getCountryId, countryId)
.orderByDesc(PayCountry::getSort)
.list()
.stream()
.filter(this::isUsablePayCountry)
.findFirst()
.orElse(null);
}
private boolean isUsablePayCountry(PayCountry payCountry) {
return Objects.nonNull(payCountry)
&& StringUtils.isNotBlank(payCountry.getCurrency())
&& CollectionUtils.isNotEmpty(
payCountryChannelDetailsService.listChannelCode(payCountry.getId()));
}
}

View File

@ -23,7 +23,6 @@ import com.red.circle.order.infra.database.rds.service.pay.PayApplicationCommodi
import com.red.circle.order.infra.database.rds.service.pay.PayApplicationService; import com.red.circle.order.infra.database.rds.service.pay.PayApplicationService;
import com.red.circle.order.infra.database.rds.service.pay.PayChannelService; import com.red.circle.order.infra.database.rds.service.pay.PayChannelService;
import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelDetailsService; import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelDetailsService;
import com.red.circle.order.infra.database.rds.service.pay.PayCountryService;
import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.num.ArithmeticUtils; import com.red.circle.tool.core.num.ArithmeticUtils;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
@ -50,9 +49,9 @@ import org.springframework.stereotype.Component;
public class PayWebApplicationCommodityV2QueryExe { public class PayWebApplicationCommodityV2QueryExe {
private final PayAppConvertor payAppConvertor; private final PayAppConvertor payAppConvertor;
private final PayCountryService payCountryService;
private final PayChannelService payChannelService; private final PayChannelService payChannelService;
private final PayApplicationService payApplicationService; private final PayApplicationService payApplicationService;
private final PayCountryAliasResolver payCountryAliasResolver;
private final PayApplicationCommodityService payApplicationCommodityService; private final PayApplicationCommodityService payApplicationCommodityService;
private final PayCountryChannelDetailsService payCountryChannelDetailsService; private final PayCountryChannelDetailsService payCountryChannelDetailsService;
@ -166,42 +165,7 @@ public class PayWebApplicationCommodityV2QueryExe {
} }
private PayCountry getPayCountry(PayWebApplicationCommodityCmd cmd) { private PayCountry getPayCountry(PayWebApplicationCommodityCmd cmd) {
if (Objects.isNull(cmd.getPayCountryId())) { return payCountryAliasResolver.resolveUsablePayCountry(cmd.getPayCountryId());
return payCountryService.getShelfSortMax();
}
PayCountry payCountry = payCountryService.getById(cmd.getPayCountryId());
if (Objects.isNull(payCountry)) {
return getUsablePayCountryByCountryId(cmd.getPayCountryId());
}
if (isUsablePayCountry(payCountry)) {
return payCountry;
}
return Optional.ofNullable(getUsablePayCountryByCountryId(payCountry.getCountryId()))
.orElse(payCountry);
}
private PayCountry getUsablePayCountryByCountryId(Long countryId) {
if (Objects.isNull(countryId)) {
return null;
}
return payCountryService.query()
.eq(PayCountry::getShelf, Boolean.TRUE)
.eq(PayCountry::getCountryId, countryId)
.orderByDesc(PayCountry::getSort)
.list()
.stream()
.filter(this::isUsablePayCountry)
.findFirst()
.orElse(null);
}
private boolean isUsablePayCountry(PayCountry payCountry) {
return Objects.nonNull(payCountry)
&& StringUtils.isNotBlank(payCountry.getCurrency())
&& CollectionUtils.isNotEmpty(
payCountryChannelDetailsService.listChannelCode(payCountry.getId()));
} }
private List<PayChannelDetailsCO> getPayChannels(List<PayChannel> payChannels, private List<PayChannelDetailsCO> getPayChannels(List<PayChannel> payChannels,

View File

@ -25,6 +25,7 @@ public class PayWebOpenPayCountryQueryExe {
private final PayCountryService payCountryService; private final PayCountryService payCountryService;
private final SysCountryCodeClient sysCountryCodeClient; private final SysCountryCodeClient sysCountryCodeClient;
private final PayCountryAliasResolver payCountryAliasResolver;
public List<PayCountryCO> execute() { public List<PayCountryCO> execute() {
List<PayCountry> payCountries = payCountryService.listAllShelf(); List<PayCountry> payCountries = payCountryService.listAllShelf();
@ -36,12 +37,18 @@ public class PayWebOpenPayCountryQueryExe {
.mapByIdes( .mapByIdes(
payCountries.stream().map(PayCountry::getCountryId).collect(Collectors.toSet())) payCountries.stream().map(PayCountry::getCountryId).collect(Collectors.toSet()))
); );
return payCountries.stream().map(payCountry -> new PayCountryCO() return payCountries.stream().map(payCountry -> {
.setId(payCountry.getId()) PayCountry compatiblePayCountry = payCountryAliasResolver
.setPayCountryId(payCountry.getId()) .resolveUsablePayCountryByCountryId(payCountry.getCountryId());
.setCountryId(payCountry.getCountryId()) if (compatiblePayCountry == null) {
.setCurrency(payCountry.getCurrency()) compatiblePayCountry = payCountry;
.setCountry(countryCodeMap.get(payCountry.getCountryId())) }
).collect(Collectors.toList()); return new PayCountryCO()
.setId(payCountry.getId())
.setPayCountryId(compatiblePayCountry.getId())
.setCountryId(payCountry.getCountryId())
.setCurrency(compatiblePayCountry.getCurrency())
.setCountry(countryCodeMap.get(payCountry.getCountryId()));
}).collect(Collectors.toList());
} }
} }

View File

@ -17,29 +17,23 @@ import com.red.circle.order.infra.database.rds.entity.pay.PayCountryChannelDetai
import com.red.circle.order.infra.database.rds.service.pay.PayApplicationCommodityService; import com.red.circle.order.infra.database.rds.service.pay.PayApplicationCommodityService;
import com.red.circle.order.infra.database.rds.service.pay.PayApplicationService; import com.red.circle.order.infra.database.rds.service.pay.PayApplicationService;
import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelDetailsService; import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelDetailsService;
import com.red.circle.order.infra.database.rds.service.pay.PayCountryService;
import com.red.circle.order.infra.util.IpUtils; import com.red.circle.order.infra.util.IpUtils;
import com.red.circle.order.inner.asserts.PayErrorCode; import com.red.circle.order.inner.asserts.PayErrorCode;
import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseReceiptType; import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseReceiptType;
import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient; import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient;
import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO; import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.num.ArithmeticUtils; import com.red.circle.tool.core.num.ArithmeticUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils; import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.other.inner.endpoint.user.region.UserRegionClient;
import com.red.circle.order.app.common.PayerMaxProperties; import com.red.circle.order.app.common.PayerMaxProperties;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import static com.red.circle.tool.core.text.StringPool.SYMBOL_COMMA;
/** /**
* web 下单处理. * web 下单处理.
* *
@ -52,9 +46,9 @@ public class PayWebPlaceAnOrderCmdExe {
private final PayProperties payProperties; private final PayProperties payProperties;
private final PayerMaxProperties payerMaxProperties; private final PayerMaxProperties payerMaxProperties;
private final PayCountryService payCountryService;
private final SysCountryCodeClient sysCountryCodeClient; private final SysCountryCodeClient sysCountryCodeClient;
private final PayApplicationService payApplicationService; private final PayApplicationService payApplicationService;
private final PayCountryAliasResolver payCountryAliasResolver;
private final PayPlaceAnOrderService payPlaceAnOrderService; private final PayPlaceAnOrderService payPlaceAnOrderService;
private final PayApplicationCommodityService payApplicationCommodityService; private final PayApplicationCommodityService payApplicationCommodityService;
private final PayCountryChannelDetailsService payCountryChannelDetailsService; private final PayCountryChannelDetailsService payCountryChannelDetailsService;
@ -71,7 +65,7 @@ public class PayWebPlaceAnOrderCmdExe {
ResponseAssert.notNull(PayErrorCode.UNKNOWN_APPLICATION, application); ResponseAssert.notNull(PayErrorCode.UNKNOWN_APPLICATION, application);
PayApplicationCommodity commodity = getAppCommodity(cmd); PayApplicationCommodity commodity = getAppCommodity(cmd);
PayCountry payCountry = getPayCountry(cmd.getPayCountryId()); PayCountry payCountry = payCountryAliasResolver.resolveUsablePayCountry(cmd.getPayCountryId());
// 不支持地区 // 不支持地区
ResponseAssert.notNull(PayErrorCode.REGION_NOT_SUPPORTED, payCountry); ResponseAssert.notNull(PayErrorCode.REGION_NOT_SUPPORTED, payCountry);
cmd.setPayCountryId(payCountry.getId()); cmd.setPayCountryId(payCountry.getId());
@ -134,41 +128,6 @@ public class PayWebPlaceAnOrderCmdExe {
return details; return details;
} }
private PayCountry getPayCountry(Long payCountryId) {
PayCountry payCountry = payCountryService.getById(payCountryId);
if (Objects.isNull(payCountry)) {
return getUsablePayCountryByCountryId(payCountryId);
}
if (isUsablePayCountry(payCountry)) {
return payCountry;
}
PayCountry compatiblePayCountry = getUsablePayCountryByCountryId(payCountry.getCountryId());
return Objects.nonNull(compatiblePayCountry) ? compatiblePayCountry : payCountry;
}
private PayCountry getUsablePayCountryByCountryId(Long countryId) {
if (Objects.isNull(countryId)) {
return null;
}
return payCountryService.query()
.eq(PayCountry::getShelf, Boolean.TRUE)
.eq(PayCountry::getCountryId, countryId)
.orderByDesc(PayCountry::getSort)
.list()
.stream()
.filter(this::isUsablePayCountry)
.findFirst()
.orElse(null);
}
private boolean isUsablePayCountry(PayCountry payCountry) {
return Objects.nonNull(payCountry)
&& StringUtils.isNotBlank(payCountry.getCurrency())
&& CollectionUtils.isNotEmpty(
payCountryChannelDetailsService.listChannelCode(payCountry.getId()));
}
/** /**
* 获取实时汇率失败时返回配置的汇率 * 获取实时汇率失败时返回配置的汇率
* @param payCountry 支付国家配置 * @param payCountry 支付国家配置

View File

@ -8,7 +8,6 @@ import com.red.circle.order.app.dto.clientobject.pay.UserProfileAndCountryCO;
import com.red.circle.order.app.dto.clientobject.pay.WebSiteUseProfileCO; import com.red.circle.order.app.dto.clientobject.pay.WebSiteUseProfileCO;
import com.red.circle.order.app.dto.cmd.PayWebUserCmd; import com.red.circle.order.app.dto.cmd.PayWebUserCmd;
import com.red.circle.order.infra.database.rds.entity.pay.PayCountry; import com.red.circle.order.infra.database.rds.entity.pay.PayCountry;
import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelDetailsService;
import com.red.circle.order.infra.database.rds.service.pay.PayCountryService; import com.red.circle.order.infra.database.rds.service.pay.PayCountryService;
import com.red.circle.order.inner.model.enums.PayApplicationCommodityType; import com.red.circle.order.inner.model.enums.PayApplicationCommodityType;
import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient; import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient;
@ -25,6 +24,7 @@ import com.red.circle.other.inner.endpoint.user.region.RegionRelationClient;
import com.red.circle.other.inner.endpoint.user.region.UserRegionClient; import com.red.circle.other.inner.endpoint.user.region.UserRegionClient;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient; import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.wallet.inner.endpoint.freight.FreightGoldClient; import com.red.circle.wallet.inner.endpoint.freight.FreightGoldClient;
import java.util.AbstractMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -44,7 +44,7 @@ public class UserProfileAndCountryQueryExe {
private final UserRegionClient userRegionClient; private final UserRegionClient userRegionClient;
private final FreightGoldClient freightGoldClient; private final FreightGoldClient freightGoldClient;
private final PayCountryService payCountryService; private final PayCountryService payCountryService;
private final PayCountryChannelDetailsService payCountryChannelDetailsService; private final PayCountryAliasResolver payCountryAliasResolver;
private final UserProfileClient userProfileClient; private final UserProfileClient userProfileClient;
private final SysCountryCodeClient sysCountryCodeClient; private final SysCountryCodeClient sysCountryCodeClient;
private final RegionRelationClient regionRelationClient; private final RegionRelationClient regionRelationClient;
@ -138,21 +138,11 @@ public class UserProfileAndCountryQueryExe {
return java.util.Collections.emptyMap(); return java.util.Collections.emptyMap();
} }
List<PayCountry> candidates = payCountryService.query() return payCountryList.stream()
.eq(PayCountry::getShelf, Boolean.TRUE) .map(country -> new AbstractMap.SimpleEntry<>(country.getCountryId(),
.in(PayCountry::getCountryId, payCountryList.stream().map(PayCountry::getCountryId) payCountryAliasResolver.resolveUsablePayCountryByCountryId(country.getCountryId())))
.collect(Collectors.toSet())) .filter(entry -> Objects.nonNull(entry.getValue()))
.orderByDesc(PayCountry::getSort) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
.list();
if (CollectionUtils.isEmpty(candidates)) {
return java.util.Collections.emptyMap();
}
return candidates.stream()
.filter(country -> StringUtils.isNotBlank(country.getCurrency()))
.filter(country -> CollectionUtils.isNotEmpty(
payCountryChannelDetailsService.listChannelCode(country.getId())))
.collect(Collectors.toMap(PayCountry::getCountryId, country -> country,
(left, right) -> left)); (left, right) -> left));
} }

View File

@ -13,7 +13,6 @@ import com.red.circle.order.infra.util.MemberInfoGenerator;
import com.red.circle.order.inner.asserts.OrderErrorCode; import com.red.circle.order.inner.asserts.OrderErrorCode;
import com.red.circle.tool.core.json.JacksonUtils; import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -23,8 +22,6 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.red.circle.tool.core.text.StringPool.SYMBOL_COMMA;
/** /**
* MiFaPay web 支付策略 * MiFaPay web 支付策略
* *
@ -127,7 +124,7 @@ public class MiFaPayWebPayPlaceAnOrderStrategy implements PayWebPlaceAnOrderStra
} }
private static String replaceCountryCode(String countryCode) { private static String replaceCountryCode(String countryCode) {
return CountryCodeAliasUtils.normalizeSaudiCode(countryCode); return CountryCodeAliasUtils.normalizePaymentCountryCode(countryCode);
} }