切换支付国家汇率同步数据源
This commit is contained in:
parent
9957dc2a27
commit
986f7507e7
@ -1,16 +1,17 @@
|
|||||||
package com.red.circle.order.inner.service.impl;
|
package com.red.circle.order.inner.service.impl;
|
||||||
|
|
||||||
import com.alibaba.nacos.shaded.com.google.common.collect.Lists;
|
import com.alibaba.nacos.shaded.com.google.common.collect.Lists;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.api.client.util.Maps;
|
import com.google.api.client.util.Maps;
|
||||||
import com.red.circle.framework.core.response.CommonErrorCode;
|
import com.red.circle.framework.core.response.CommonErrorCode;
|
||||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||||
import com.red.circle.framework.dto.PageResult;
|
import com.red.circle.framework.dto.PageResult;
|
||||||
import com.red.circle.framework.mybatis.constant.PageConstant;
|
import com.red.circle.framework.mybatis.constant.PageConstant;
|
||||||
import com.red.circle.order.app.command.pay.web.strategy.PayPlaceAnOrderService;
|
|
||||||
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.entity.pay.PayCountryChannel;
|
import com.red.circle.order.infra.database.rds.entity.pay.PayCountryChannel;
|
||||||
import com.red.circle.order.infra.database.rds.entity.pay.PayCountryChannelDetails;
|
import com.red.circle.order.infra.database.rds.entity.pay.PayCountryChannelDetails;
|
||||||
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.PayCountryChannelService;
|
import com.red.circle.order.infra.database.rds.service.pay.PayCountryChannelService;
|
||||||
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.convertor.WebPayInnerConvertor;
|
import com.red.circle.order.inner.convertor.WebPayInnerConvertor;
|
||||||
@ -31,8 +32,15 @@ import com.red.circle.tool.core.date.TimestampUtils;
|
|||||||
import com.red.circle.tool.core.text.StringUtils;
|
import com.red.circle.tool.core.text.StringUtils;
|
||||||
import com.red.circle.other.inner.enums.user.RegionRelationGroupEnum;
|
import com.red.circle.other.inner.enums.user.RegionRelationGroupEnum;
|
||||||
import com.red.circle.other.inner.endpoint.user.region.RegionRelationClient;
|
import com.red.circle.other.inner.endpoint.user.region.RegionRelationClient;
|
||||||
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -43,22 +51,37 @@ import java.util.Objects;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开通支付国家.
|
* 开通支付国家.
|
||||||
*
|
*
|
||||||
* @author pengliang on 2023/11/3
|
* @author pengliang on 2023/11/3
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class PayCountryClientServiceImpl implements PayCountryClientService {
|
public class PayCountryClientServiceImpl implements PayCountryClientService {
|
||||||
|
|
||||||
private static final String BASE_CURRENCY = "USD";
|
private static final String BASE_CURRENCY = "USD";
|
||||||
private static final String SOURCE_NAME = "ISO4217/PayerMax";
|
private static final String SOURCE_NAME = "ISO4217/PublicExchangeRate";
|
||||||
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
||||||
private static final int RATE_SCALE = 8;
|
private static final int RATE_SCALE = 8;
|
||||||
|
private static final Duration EXCHANGE_RATE_TIMEOUT = Duration.ofSeconds(8);
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
private static final List<ExchangeRateSource> EXCHANGE_RATE_SOURCES = List.of(
|
||||||
|
new ExchangeRateSource("open.er-api.com", "https://open.er-api.com/v6/latest/USD",
|
||||||
|
ExchangeRateSourceType.OPEN_EXCHANGE_RATE),
|
||||||
|
new ExchangeRateSource("currency-api",
|
||||||
|
"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json",
|
||||||
|
ExchangeRateSourceType.FAWAZ_CURRENCY_API),
|
||||||
|
new ExchangeRateSource("floatrates.com", "https://www.floatrates.com/daily/usd.json",
|
||||||
|
ExchangeRateSourceType.FLOAT_RATES),
|
||||||
|
new ExchangeRateSource("frankfurter.dev",
|
||||||
|
"https://api.frankfurter.dev/v1/latest?base=USD",
|
||||||
|
ExchangeRateSourceType.FRANKFURTER));
|
||||||
|
|
||||||
private final PayCountryService payCountryService;
|
private final PayCountryService payCountryService;
|
||||||
private final SysCountryCodeClient countryCodeClient;
|
private final SysCountryCodeClient countryCodeClient;
|
||||||
@ -67,7 +90,9 @@ public class PayCountryClientServiceImpl implements PayCountryClientService {
|
|||||||
private final SysCountryCodeClient sysCountryCodeClient;
|
private final SysCountryCodeClient sysCountryCodeClient;
|
||||||
private final PayCountryChannelService payCountryChannelService;
|
private final PayCountryChannelService payCountryChannelService;
|
||||||
private final PayCountryChannelDetailsService payCountryChannelDetailsService;
|
private final PayCountryChannelDetailsService payCountryChannelDetailsService;
|
||||||
private final PayPlaceAnOrderService payPlaceAnOrderService;
|
private final HttpClient exchangeRateHttpClient = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(EXCHANGE_RATE_TIMEOUT)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -316,23 +341,24 @@ public class PayCountryClientServiceImpl implements PayCountryClientService {
|
|||||||
.filter(currency -> !Objects.equals(BASE_CURRENCY, currency))
|
.filter(currency -> !Objects.equals(BASE_CURRENCY, currency))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
Map<String, BigDecimal> rateMap = Maps.newHashMap();
|
ExchangeRateFetchResult exchangeRateFetchResult = fetchLatestUsdRates(targetCurrencies);
|
||||||
rateMap.put(BASE_CURRENCY, BigDecimal.ONE);
|
Map<String, BigDecimal> rateMap = exchangeRateFetchResult.getRates();
|
||||||
rateMap.putAll(payPlaceAnOrderService.getRealTimeExchangeRates(BASE_CURRENCY, targetCurrencies));
|
|
||||||
|
|
||||||
List<PayCountry> updates = payCountries.stream()
|
List<PayCountry> updates = payCountries.stream()
|
||||||
.map(payCountry -> buildSyncUpdate(payCountry, countryMap.get(payCountry.getCountryId()),
|
.map(payCountry -> buildSyncUpdate(payCountry, countryMap.get(payCountry.getCountryId()),
|
||||||
currencyMap.get(payCountry.getId()), rateMap, markupMultiplier, skippedCountries))
|
currencyMap.get(payCountry.getId()), rateMap, markupMultiplier, skippedCountries))
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE,
|
if (!CollectionUtils.isEmpty(updates)) {
|
||||||
payCountryService.updateCurrencyRates(updates));
|
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE,
|
||||||
|
payCountryService.updateCurrencyRates(updates));
|
||||||
|
}
|
||||||
|
|
||||||
return new PayCountryCurrencyRateSyncDTO()
|
return new PayCountryCurrencyRateSyncDTO()
|
||||||
.setTotalCount(payCountries.size())
|
.setTotalCount(payCountries.size())
|
||||||
.setUpdatedCount(updates.size())
|
.setUpdatedCount(updates.size())
|
||||||
.setSkippedCount(skippedCountries.size())
|
.setSkippedCount(skippedCountries.size())
|
||||||
.setSourceName(SOURCE_NAME)
|
.setSourceName(exchangeRateFetchResult.getSourceName())
|
||||||
.setSkippedCountries(skippedCountries);
|
.setSkippedCountries(skippedCountries);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,12 +374,132 @@ public class PayCountryClientServiceImpl implements PayCountryClientService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
payCountry.setCurrency(currency);
|
payCountry.setCurrency(currency);
|
||||||
payCountry.setUsdExchangeRate(rate.multiply(markupMultiplier).setScale(RATE_SCALE,
|
payCountry.setUsdExchangeRate(roundUpRate(rate.multiply(markupMultiplier)));
|
||||||
RoundingMode.HALF_UP));
|
|
||||||
payCountry.setUpdateTime(TimestampUtils.now());
|
payCountry.setUpdateTime(TimestampUtils.now());
|
||||||
return payCountry;
|
return payCountry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ExchangeRateFetchResult fetchLatestUsdRates(Set<String> targetCurrencies) {
|
||||||
|
Map<String, BigDecimal> rateMap = Maps.newHashMap();
|
||||||
|
rateMap.put(BASE_CURRENCY, BigDecimal.ONE);
|
||||||
|
if (CollectionUtils.isEmpty(targetCurrencies)) {
|
||||||
|
return new ExchangeRateFetchResult(rateMap, SOURCE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> missingCurrencies = targetCurrencies.stream()
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.map(currency -> currency.trim().toUpperCase(Locale.ROOT))
|
||||||
|
.filter(currency -> !Objects.equals(BASE_CURRENCY, currency))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (CollectionUtils.isEmpty(missingCurrencies)) {
|
||||||
|
return new ExchangeRateFetchResult(rateMap, SOURCE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> sourceNames = new ArrayList<>();
|
||||||
|
for (ExchangeRateSource source : EXCHANGE_RATE_SOURCES) {
|
||||||
|
if (CollectionUtils.isEmpty(missingCurrencies)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Map<String, BigDecimal> sourceRates = fetchRatesFromSource(source);
|
||||||
|
int beforeSize = missingCurrencies.size();
|
||||||
|
missingCurrencies.removeIf(currency -> {
|
||||||
|
BigDecimal rate = sourceRates.get(currency);
|
||||||
|
if (Objects.isNull(rate) || rate.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rateMap.put(currency, rate);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (missingCurrencies.size() < beforeSize) {
|
||||||
|
sourceNames.add(source.getName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("同步支付国家汇率源失败, source={}", source.getName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String sourceName = sourceNames.isEmpty()
|
||||||
|
? SOURCE_NAME
|
||||||
|
: SOURCE_NAME + "/" + String.join(",", sourceNames);
|
||||||
|
return new ExchangeRateFetchResult(rateMap, sourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, BigDecimal> fetchRatesFromSource(ExchangeRateSource source)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(source.getUrl()))
|
||||||
|
.timeout(EXCHANGE_RATE_TIMEOUT)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> response = exchangeRateHttpClient.send(request,
|
||||||
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
|
throw new IOException("exchange rate source http status " + response.statusCode());
|
||||||
|
}
|
||||||
|
JsonNode root = OBJECT_MAPPER.readTree(response.body());
|
||||||
|
return switch (source.getType()) {
|
||||||
|
case OPEN_EXCHANGE_RATE -> parseStandardRates(root.path("rates"));
|
||||||
|
case FAWAZ_CURRENCY_API -> parseStandardRates(root.path("usd"));
|
||||||
|
case FLOAT_RATES -> parseFloatRates(root);
|
||||||
|
case FRANKFURTER -> parseStandardRates(root.path("rates"));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, BigDecimal> parseStandardRates(JsonNode ratesNode) {
|
||||||
|
Map<String, BigDecimal> rateMap = Maps.newHashMap();
|
||||||
|
if (Objects.isNull(ratesNode) || !ratesNode.isObject()) {
|
||||||
|
return rateMap;
|
||||||
|
}
|
||||||
|
ratesNode.fields().forEachRemaining(entry -> {
|
||||||
|
BigDecimal rate = readPositiveRate(entry.getValue());
|
||||||
|
if (Objects.nonNull(rate)) {
|
||||||
|
rateMap.put(entry.getKey().trim().toUpperCase(Locale.ROOT), rate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return rateMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, BigDecimal> parseFloatRates(JsonNode root) {
|
||||||
|
Map<String, BigDecimal> rateMap = Maps.newHashMap();
|
||||||
|
if (Objects.isNull(root) || !root.isObject()) {
|
||||||
|
return rateMap;
|
||||||
|
}
|
||||||
|
root.fields().forEachRemaining(entry -> {
|
||||||
|
JsonNode item = entry.getValue();
|
||||||
|
String currency = Optional.ofNullable(item.path("alphaCode").asText(null))
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.orElseGet(() -> Optional.ofNullable(item.path("code").asText(null))
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.orElse(entry.getKey()));
|
||||||
|
BigDecimal rate = readPositiveRate(item.path("rate"));
|
||||||
|
if (StringUtils.isNotBlank(currency) && Objects.nonNull(rate)) {
|
||||||
|
rateMap.put(currency.trim().toUpperCase(Locale.ROOT), rate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return rateMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal readPositiveRate(JsonNode node) {
|
||||||
|
if (Objects.isNull(node) || node.isMissingNode() || node.isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
BigDecimal rate = new BigDecimal(node.asText());
|
||||||
|
return rate.compareTo(BigDecimal.ZERO) > 0 ? rate : null;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal roundUpRate(BigDecimal rate) {
|
||||||
|
// 与参考项目保持一致:先叠加上浮比例,再向上保留 1 位小数,避免后台显示低于实时成本。
|
||||||
|
return rate.movePointRight(1)
|
||||||
|
.setScale(0, RoundingMode.CEILING)
|
||||||
|
.movePointLeft(1)
|
||||||
|
.setScale(1);
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveCountryCurrency(SysCountryCodeDTO country) {
|
private String resolveCountryCurrency(SysCountryCodeDTO country) {
|
||||||
if (Objects.isNull(country) || StringUtils.isBlank(country.getAlphaTwo())) {
|
if (Objects.isNull(country) || StringUtils.isBlank(country.getAlphaTwo())) {
|
||||||
return "";
|
return "";
|
||||||
@ -381,6 +527,57 @@ public class PayCountryClientServiceImpl implements PayCountryClientService {
|
|||||||
return countryName + "(" + payCountry.getId() + "): " + reason;
|
return countryName + "(" + payCountry.getId() + "): " + reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum ExchangeRateSourceType {
|
||||||
|
OPEN_EXCHANGE_RATE,
|
||||||
|
FAWAZ_CURRENCY_API,
|
||||||
|
FLOAT_RATES,
|
||||||
|
FRANKFURTER
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ExchangeRateSource {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final String url;
|
||||||
|
private final ExchangeRateSourceType type;
|
||||||
|
|
||||||
|
private ExchangeRateSource(String name, String url, ExchangeRateSourceType type) {
|
||||||
|
this.name = name;
|
||||||
|
this.url = url;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExchangeRateSourceType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ExchangeRateFetchResult {
|
||||||
|
|
||||||
|
private final Map<String, BigDecimal> rates;
|
||||||
|
private final String sourceName;
|
||||||
|
|
||||||
|
private ExchangeRateFetchResult(Map<String, BigDecimal> rates, String sourceName) {
|
||||||
|
this.rates = rates;
|
||||||
|
this.sourceName = sourceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, BigDecimal> getRates() {
|
||||||
|
return rates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSourceName() {
|
||||||
|
return sourceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PayCountryDTO> listPayCountryByRegionId(String regionId) {
|
public List<PayCountryDTO> listPayCountryByRegionId(String regionId) {
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user