From 6b78840f5eca79e57eec39e24a11ee8e5df4bfb0 Mon Sep 17 00:00:00 2001 From: tianfeng <769204422@qq.com> Date: Fri, 26 Sep 2025 16:43:16 +0800 Subject: [PATCH] =?UTF-8?q?recharge=E6=8E=A5=E5=85=A5=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E6=B1=87=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pay/web/PayWebPlaceAnOrderCmdExe.java | 34 ++++++- .../web/strategy/PayPlaceAnOrderService.java | 67 ++++++++++++++ .../order/app/common/PayerMaxProperties.java | 71 +++++++++++++++ .../order/app/common/QuoteQueryParam.java | 75 ++++++++++++++++ .../order/app/common/QuoteQueryResponse.java | 89 +++++++++++++++++++ 5 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/PayerMaxProperties.java create mode 100644 rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/QuoteQueryParam.java create mode 100644 rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/QuoteQueryResponse.java diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebPlaceAnOrderCmdExe.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebPlaceAnOrderCmdExe.java index df4ebd78..37c014b8 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebPlaceAnOrderCmdExe.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/PayWebPlaceAnOrderCmdExe.java @@ -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( diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/PayPlaceAnOrderService.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/PayPlaceAnOrderService.java index 912079e5..adf45e2e 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/PayPlaceAnOrderService.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/web/strategy/PayPlaceAnOrderService.java @@ -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; + } + } + } diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/PayerMaxProperties.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/PayerMaxProperties.java new file mode 100644 index 00000000..e372daf6 --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/PayerMaxProperties.java @@ -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; + } +} diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/QuoteQueryParam.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/QuoteQueryParam.java new file mode 100644 index 00000000..771d8d38 --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/QuoteQueryParam.java @@ -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 ccyPairList; + + /** + * 价格类型列表,买入价BID,卖出价OFFER,默认查询全部 + */ + @JsonProperty("priceTypeList") + private List 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")); + } +} diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/QuoteQueryResponse.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/QuoteQueryResponse.java new file mode 100644 index 00000000..05b84379 --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/QuoteQueryResponse.java @@ -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 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; + } +}