切换支付国家汇率同步数据源
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.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.api.client.util.Maps;
|
||||
import com.red.circle.framework.core.response.CommonErrorCode;
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.framework.dto.PageResult;
|
||||
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.PayCountryChannel;
|
||||
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.PayCountryService;
|
||||
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.other.inner.enums.user.RegionRelationGroupEnum;
|
||||
import com.red.circle.other.inner.endpoint.user.region.RegionRelationClient;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
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.Currency;
|
||||
import java.util.List;
|
||||
@ -43,22 +51,37 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 开通支付国家.
|
||||
*
|
||||
* @author pengliang on 2023/11/3
|
||||
*/
|
||||
@Service
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class PayCountryClientServiceImpl implements PayCountryClientService {
|
||||
|
||||
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 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 SysCountryCodeClient countryCodeClient;
|
||||
@ -67,7 +90,9 @@ public class PayCountryClientServiceImpl implements PayCountryClientService {
|
||||
private final SysCountryCodeClient sysCountryCodeClient;
|
||||
private final PayCountryChannelService payCountryChannelService;
|
||||
private final PayCountryChannelDetailsService payCountryChannelDetailsService;
|
||||
private final PayPlaceAnOrderService payPlaceAnOrderService;
|
||||
private final HttpClient exchangeRateHttpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(EXCHANGE_RATE_TIMEOUT)
|
||||
.build();
|
||||
|
||||
|
||||
@Override
|
||||
@ -316,23 +341,24 @@ public class PayCountryClientServiceImpl implements PayCountryClientService {
|
||||
.filter(currency -> !Objects.equals(BASE_CURRENCY, currency))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<String, BigDecimal> rateMap = Maps.newHashMap();
|
||||
rateMap.put(BASE_CURRENCY, BigDecimal.ONE);
|
||||
rateMap.putAll(payPlaceAnOrderService.getRealTimeExchangeRates(BASE_CURRENCY, targetCurrencies));
|
||||
ExchangeRateFetchResult exchangeRateFetchResult = fetchLatestUsdRates(targetCurrencies);
|
||||
Map<String, BigDecimal> rateMap = exchangeRateFetchResult.getRates();
|
||||
|
||||
List<PayCountry> updates = payCountries.stream()
|
||||
.map(payCountry -> buildSyncUpdate(payCountry, countryMap.get(payCountry.getCountryId()),
|
||||
currencyMap.get(payCountry.getId()), rateMap, markupMultiplier, skippedCountries))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE,
|
||||
payCountryService.updateCurrencyRates(updates));
|
||||
if (!CollectionUtils.isEmpty(updates)) {
|
||||
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE,
|
||||
payCountryService.updateCurrencyRates(updates));
|
||||
}
|
||||
|
||||
return new PayCountryCurrencyRateSyncDTO()
|
||||
.setTotalCount(payCountries.size())
|
||||
.setUpdatedCount(updates.size())
|
||||
.setSkippedCount(skippedCountries.size())
|
||||
.setSourceName(SOURCE_NAME)
|
||||
.setSourceName(exchangeRateFetchResult.getSourceName())
|
||||
.setSkippedCountries(skippedCountries);
|
||||
}
|
||||
|
||||
@ -348,12 +374,132 @@ public class PayCountryClientServiceImpl implements PayCountryClientService {
|
||||
return null;
|
||||
}
|
||||
payCountry.setCurrency(currency);
|
||||
payCountry.setUsdExchangeRate(rate.multiply(markupMultiplier).setScale(RATE_SCALE,
|
||||
RoundingMode.HALF_UP));
|
||||
payCountry.setUsdExchangeRate(roundUpRate(rate.multiply(markupMultiplier)));
|
||||
payCountry.setUpdateTime(TimestampUtils.now());
|
||||
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) {
|
||||
if (Objects.isNull(country) || StringUtils.isBlank(country.getAlphaTwo())) {
|
||||
return "";
|
||||
@ -381,6 +527,57 @@ public class PayCountryClientServiceImpl implements PayCountryClientService {
|
||||
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
|
||||
public List<PayCountryDTO> listPayCountryByRegionId(String regionId) {
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user