recharge接入实时汇率

This commit is contained in:
tianfeng 2025-09-26 16:43:16 +08:00
parent 860ebd5a21
commit 6b78840f5e
5 changed files with 335 additions and 1 deletions

View File

@ -26,9 +26,11 @@ import com.red.circle.tool.core.num.ArithmeticUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.other.inner.endpoint.user.region.UserRegionClient;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
@ -36,6 +38,7 @@ import org.springframework.stereotype.Component;
*
* @author pengliang on 2021/7/9
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PayWebPlaceAnOrderCmdExe {
@ -82,9 +85,11 @@ public class PayWebPlaceAnOrderCmdExe {
details.setSysOrigin(application.getAppCode());
details.setAcceptUserId(cmd.getUserId());
details.setCreateUserId(cmd.getUserId());
// 获取实时汇率失败时使用配置的汇率作为备选
BigDecimal exchangeRate = getRealTimeExchangeRate(payCountry);
details.setAmount(PayAmountArithmeticUtils.multiplyUpNotPoint(
commodity.getAmountUsd(),
payCountry.getUsdExchangeRate()
exchangeRate
)
);
details.setAmountUsd(commodity.getAmountUsd());
@ -120,6 +125,33 @@ public class PayWebPlaceAnOrderCmdExe {
return details;
}
/**
* 获取实时汇率失败时返回配置的汇率
* @param payCountry 支付国家配置
* @return 汇率
*/
private BigDecimal getRealTimeExchangeRate(PayCountry payCountry) {
BigDecimal realTimeRate = null;
try {
// 调用实时汇率查询 - 这里假设目标币种是payCountry对应的币种
// 您可以根据实际需要调整币种参数
realTimeRate = payPlaceAnOrderService.getRealTimeExchangeRate("USD", payCountry.getCurrency());
} catch (Exception e) {
log.warn("Failed to get real-time exchange rate for country {}, will use configured rate as fallback",
payCountry.getId(), e);
}
// 如果实时汇率获取失败使用配置的汇率作为备选
BigDecimal exchangeRate = realTimeRate != null ? realTimeRate : payCountry.getUsdExchangeRate();
// 记录使用的汇率来源
log.info("Using {} exchange rate: {} for country {} amount calculation",
realTimeRate != null ? "real-time" : "configured",
exchangeRate, payCountry.getId());
return exchangeRate;
}
private void rechargeRecord(PayPlaceAnOrderDetailsV2 details) {
if (Objects.equals(details.getNewVersion(), Boolean.TRUE)) {
userFreightRechargeRecordGateway.inrNowMonthAmount(

View File

@ -1,8 +1,12 @@
package com.red.circle.order.app.command.pay.web.strategy;
import com.google.api.client.util.Maps;
import com.red.circle.component.pay.paymax.PayMaxUtils;
import com.red.circle.framework.core.request.RequestClientEnum;
import com.red.circle.framework.web.props.EnvProperties;
import com.red.circle.order.app.common.PayerMaxProperties;
import com.red.circle.order.app.common.QuoteQueryParam;
import com.red.circle.order.app.common.QuoteQueryResponse;
import com.red.circle.order.app.dto.clientobject.pay.PlaceAnOrderResponseCO;
import com.red.circle.order.infra.database.mongo.order.InAppPurchaseDetailsService;
import com.red.circle.order.infra.database.mongo.order.entity.InAppPurchaseDetails;
@ -11,13 +15,19 @@ import com.red.circle.order.infra.database.mongo.order.entity.assist.InAppPurcha
import com.red.circle.order.infra.database.mongo.order.entity.assist.InAppPurchaseStatusStep;
import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseStatus;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.http.RcHttpClient;
import com.red.circle.tool.core.json.JacksonUtils;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author pengliang on 2022/11/2
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PayPlaceAnOrderService {
@ -25,6 +35,8 @@ public class PayPlaceAnOrderService {
private final EnvProperties envProperties;
private final InAppPurchaseDetailsService inAppPurchaseDetailsService;
private final FactoryPlaceAnOrderStrategyV2 factoryPlaceAnOrderStrategyV2;
private final PayerMaxProperties payerMaxProperties;
private final RcHttpClient rcHttpClient = RcHttpClient.builder().build();
public PlaceAnOrderResponseCO doRequest(PayPlaceAnOrderDetailsV2 param) {
// 第三方请求下单
@ -85,4 +97,59 @@ public class PayPlaceAnOrderService {
return envProperties.isProd() ? PayEnv.PROD.name() : PayEnv.TEST.name();
}
/**
* 查询实时汇率
* @param baseCurrency 基础币种如USD
* @param targetCurrency 目标币种如CNY
* @return 汇率值
*/
public BigDecimal getRealTimeExchangeRate(String baseCurrency, String targetCurrency) {
try {
QuoteQueryParam param = new QuoteQueryParam();
QuoteQueryParam.QuoteQueryData data = new QuoteQueryParam.QuoteQueryData();
// 币种对格式基础币种+目标币种如USDCNY
String ccyPair = baseCurrency + targetCurrency;
data.setCcyPairList(Arrays.asList(ccyPair));
data.setPriceTypeList(Arrays.asList("OFFER")); // 使用卖出价
param.setAppId(payerMaxProperties.getAppId());
param.setMerchantNo(payerMaxProperties.getMerchantId());
param.setData(data);
String url = payerMaxProperties.getGatewayBaseUrl() + "/aggregate-pay/api/gateway/quoteQuery";
String jsonParam = JacksonUtils.toJson(param);
String signature = PayMaxUtils.signRsa(jsonParam, payerMaxProperties.getRsaPrivateKey());
if (!envProperties.isProd()) {
log.info("Exchange Rate Query Request: {}\nParam:{}\nSign:{}", url, jsonParam, signature);
}
QuoteQueryResponse response = rcHttpClient.post()
.uri(url)
.header("sign", signature)
.contentType("application/json")
.bodyValue(jsonParam)
.retrieve()
.bodyToEntity(QuoteQueryResponse.class)
.block();
if (response != null && response.isSuccess() && response.getData() != null
&& response.getData().getRateList() != null && !response.getData().getRateList().isEmpty()) {
// 使用offerRate卖出价作为汇率
BigDecimal rate = response.getData().getRateList().get(0).getOfferRate();
log.info("Successfully got real-time exchange rate from {} to {}: {}", baseCurrency, targetCurrency, rate);
return rate;
} else {
log.error("Failed to get real-time exchange rate, response: {}",
response != null ? JacksonUtils.toJson(response) : "null");
return null;
}
} catch (Exception e) {
log.error("Error occurred while querying real-time exchange rate from {} to {}",
baseCurrency, targetCurrency, e);
return null;
}
}
}

View File

@ -0,0 +1,71 @@
package com.red.circle.order.app.common;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* PayerMax配置属性
*
* @author system
*/
@Data
@Component
@ConfigurationProperties(prefix = "red-circle.pay.payer-max")
public class PayerMaxProperties {
private String secretKey;
private String merchantId;
private String gatewayBaseUrl;
private String rsaPrivateKey;
private String platformRsaPublicKey;
private String appId;
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public String getMerchantId() {
return merchantId;
}
public void setMerchantId(String merchantId) {
this.merchantId = merchantId;
}
public String getGatewayBaseUrl() {
return gatewayBaseUrl;
}
public void setGatewayBaseUrl(String gatewayBaseUrl) {
this.gatewayBaseUrl = gatewayBaseUrl;
}
public String getRsaPrivateKey() {
return rsaPrivateKey;
}
public void setRsaPrivateKey(String rsaPrivateKey) {
this.rsaPrivateKey = rsaPrivateKey;
}
public String getPlatformRsaPublicKey() {
return platformRsaPublicKey;
}
public void setPlatformRsaPublicKey(String platformRsaPublicKey) {
this.platformRsaPublicKey = platformRsaPublicKey;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
}

View File

@ -0,0 +1,75 @@
package com.red.circle.order.app.common;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* PayerMax汇率查询请求参数
*
* @author system
*/
@Data
public class QuoteQueryParam {
/**
* 接口版本 当前值为1.2
*/
@JsonProperty("version")
private String version = "1.2";
/**
* 密钥版本 当前值为1
*/
@JsonProperty("keyVersion")
private String keyVersion = "1";
/**
* 请求时间符合rfc3339规范格式yyyy-MM-dd'T'HH:mm:ss.SSSXXX
*/
@JsonProperty("requestTime")
private String requestTime;
/**
* 商户应用Id
*/
@JsonProperty("appId")
private String appId;
/**
* 商户号
*/
@JsonProperty("merchantNo")
private String merchantNo;
/**
* 数据部分
*/
@JsonProperty("data")
private QuoteQueryData data;
@Data
public static class QuoteQueryData {
/**
* 币种对列表基于美金标价法拼接USD在前例如:USDIDR
*/
@JsonProperty("ccyPairList")
private List<String> ccyPairList;
/**
* 价格类型列表买入价BID,卖出价OFFER,默认查询全部
*/
@JsonProperty("priceTypeList")
private List<String> priceTypeList;
}
public QuoteQueryParam() {
// 设置当前时间格式yyyy-MM-dd'T'HH:mm:ss.SSSXXX示例2022-06-10T20:30:53.732+08:00
this.requestTime = OffsetDateTime.now(ZoneId.of("+08:00"))
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"));
}
}

View File

@ -0,0 +1,89 @@
package com.red.circle.order.app.common;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* PayerMax汇率查询响应
*
* @author system
*/
@Data
public class QuoteQueryResponse {
/**
* 返回码'APPLY_SUCCESS'代表成功
*/
@JsonProperty("code")
private String code;
/**
* 返回描述
*/
@JsonProperty("msg")
private String msg;
/**
* 响应数据
*/
@JsonProperty("data")
private ResponseData data;
/**
* 判断是否成功
*/
public boolean isSuccess() {
return "APPLY_SUCCESS".equals(code);
}
@Data
public static class ResponseData {
/**
* 汇率列表
*/
@JsonProperty("rateList")
private List<RateInfo> rateList;
}
@Data
public static class RateInfo {
/**
* 币种对
*/
@JsonProperty("ccyPair")
private String ccyPair;
/**
* 买入价默认8位小数
*/
@JsonProperty("bidRate")
private BigDecimal bidRate;
/**
* 卖出价默认8位小数
*/
@JsonProperty("offerRate")
private BigDecimal offerRate;
/**
* 汇率时间格式yyyy-MM-dd'T'HH:mm:ss.SSSXXX
*/
@JsonProperty("rateTime")
private String rateTime;
/**
* 版本
*/
@JsonProperty("version")
private String version;
/**
* 过期时间格式yyyy-MM-dd'T'HH:mm:ss.SSSXXX
*/
@JsonProperty("expiryTime")
private String expiryTime;
}
}