From 1bb08855fb71a0b559dabf2927f1be26f587cfdb Mon Sep 17 00:00:00 2001 From: old burden Date: Tue, 20 Jan 2026 16:54:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=80=9A=E8=BF=87=E5=85=8D=E8=B4=B9?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=8D=A2=E7=AE=97=E6=B1=87=E7=8E=87=EF=BC=8C?= =?UTF-8?q?=E5=A6=82=E6=9E=9C=E5=A4=B1=E6=95=88=E4=BC=9A=E5=8F=96=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 13 + .../ai/service/IExchangeRateService.java | 33 ++ .../service/impl/ExchangeRateServiceImpl.java | 292 ++++++++++++++++++ .../ruoyi/ai/service/impl/JinShaService.java | 5 +- .../ruoyi/ai/service/impl/KaDaService.java | 5 +- 5 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 ruoyi-system/src/main/java/com/ruoyi/ai/service/IExchangeRateService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ExchangeRateServiceImpl.java diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 70e9966..7a72af4 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -243,3 +243,16 @@ yuzhou: secretKey: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL7U/f7yb9Z9j56dauCUb0B/I0ONAcZDK+TtOnAgLEjV4qrirYHYuCumxYbPFvt6qYggjbBpphFhihbWzf8IPS7iD6VXoSX1T1iAlWFL1ZaBscqQSvPxFGtgpTBiRFS3KkZV70WswcCL770OCkY8+DJpdyk9OkD6vBa0TYU1wxJFAgMBAAECgYBpF+t5iBJHUYbSl2bQn25VWq8U+IbNpRh7TposPculoQTfj052f9+NSp7liw7hF8Bdk2/0g3pNgCYIRevUU7k9MEIKqHCiOWkyavtsfqGYI37PZ4/0uMzB5eibTqKTEkcyskSJ9GxrL4uGKgTGNc213i3VOcZZ4xEfvuDQCHF8gQJBAPATuhQeFNNAIE9TkGiESHFGChSZZgzp1xfGrAt8BwidBSe+r9duAcGJSeNJatxneeu0w6NuwQ6iq9ztnqtoG90CQQDLfSHrOTloHSa+DdBc2SFQWa/P4K2Tznb8Y5ng8L/t+a9sYvGjWOln0R3Bq9TrImm0AjWnq/saaMg2nYD1wr2JAkA36XA5xTO2a0XbE6wbG0u/zb8FQyCIO2GTsPpahl0g/Wi48+kB9CXGjBHANFYF1LeJVIUHqACgRvRdtJ1ycAGlAkEAqASVWiTw2p+fWrQLRG7gS/kR6uIIUI/cvT78UrhWsYdFqof0Hz0N0/PdzwkzkEbk4oYkiWK+viqgjj/0uHfoiQJAQhPYVVLHD7xiJApc/Aga6g0OFF5O7zy8KTsq+KTXqRlJREBH5nirSponHwYalEbUvtQrVs+Z4BCBEGCU8m2GEw== redirectUrl: www.google.com callbackUrl: www.google.com + +# 汇率服务配置 +exchange-rate: + enabled: true + apikey: 0bed27315c87475f8dd6a0792a632cc5 + api-url: https://api.currencyfreaks.com/v2.0/rates/latest + # base 货币(API返回的基准货币,通常为USD) + base-currency: USD + # 备用汇率(当API调用失败时使用) + fallback-rate-jinsha: 90 + fallback-rate-kada: 60 + # PHP比索的备用汇率(当API调用失败时使用,默认使用JinSha的汇率) + fallback-rate-php: 90 \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/ai/service/IExchangeRateService.java b/ruoyi-system/src/main/java/com/ruoyi/ai/service/IExchangeRateService.java new file mode 100644 index 0000000..5f57e98 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/ai/service/IExchangeRateService.java @@ -0,0 +1,33 @@ +package com.ruoyi.ai.service; + +import java.math.BigDecimal; + +/** + * 汇率服务接口 + * + * @author system + * @date 2025-01-XX + */ +public interface IExchangeRateService { + + /** + * 获取汇率 + * + * @param fromCurrency 源货币代码(如:CNY) + * @param toCurrency 目标货币代码(如:PHP) + * @return 汇率值 + * @throws Exception 获取汇率失败时抛出异常 + */ + BigDecimal getExchangeRate(String fromCurrency, String toCurrency) throws Exception; + + /** + * 转换金额 + * + * @param amount 原始金额 + * @param fromCurrency 源货币代码(如:CNY) + * @param toCurrency 目标货币代码(如:PHP) + * @return 转换后的金额 + * @throws Exception 获取汇率失败时抛出异常 + */ + BigDecimal convertAmount(BigDecimal amount, String fromCurrency, String toCurrency) throws Exception; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ExchangeRateServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ExchangeRateServiceImpl.java new file mode 100644 index 0000000..6285a29 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/ExchangeRateServiceImpl.java @@ -0,0 +1,292 @@ +package com.ruoyi.ai.service.impl; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; +import com.ruoyi.ai.service.IExchangeRateService; +import com.ruoyi.common.utils.http.OkHttpUtils; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 汇率服务实现类 + * + * @author system + * @date 2025-01-XX + */ +@Service +@Slf4j +public class ExchangeRateServiceImpl implements IExchangeRateService { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Value("${exchange-rate.api-url:https://api.currencyfreaks.com/v2.0/rates/latest}") + private String apiUrl; + + @Value("${exchange-rate.apikey:}") + private String apikey; + + @Value("${exchange-rate.base-currency:USD}") + private String baseCurrency; + + @Value("${exchange-rate.enabled:true}") + private boolean enabled; + + @Value("${exchange-rate.fallback-rate-jinsha:90}") + private BigDecimal fallbackRateJinSha; + + @Value("${exchange-rate.fallback-rate-kada:60}") + private BigDecimal fallbackRateKaDa; + + @Value("${exchange-rate.fallback-rate-php:90}") + private BigDecimal fallbackRatePHP; + + @Override + public BigDecimal getExchangeRate(String fromCurrency, String toCurrency) throws Exception { + if (!enabled) { + log.warn("汇率服务未启用,使用默认汇率"); + return getFallbackRate(toCurrency); + } + + try { + // 构建请求URL - 使用 CurrencyFreaks Rates Latest Endpoint + String url = buildApiUrl(); + + // 构建请求 + Request request = new Request.Builder() + .url(url) + .get() + .build(); + + // 发送请求 + Response response = OkHttpUtils.newCall(request).execute(); + ResponseBody body = response.body(); + + if (response.isSuccessful() && body != null) { + String responseBody = body.string(); + log.debug("汇率API响应: {}", responseBody); + + // 解析响应 - CurrencyFreaks Rates Latest Endpoint 响应格式 + RatesResponse ratesResponse = objectMapper.readValue(responseBody, RatesResponse.class); + + if (ratesResponse != null && ratesResponse.getRates() != null) { + BigDecimal rate = calculateRate(ratesResponse, fromCurrency, toCurrency); + if (rate != null && rate.compareTo(BigDecimal.ZERO) > 0) { + log.info("成功获取汇率 {} -> {}: {}", fromCurrency, toCurrency, rate); + return rate; + } + } + } else { + log.warn("汇率API请求失败,状态码: {}", response.code()); + if (body != null) { + log.warn("响应内容: {}", body.string()); + } + } + } catch (Exception e) { + log.error("获取汇率失败,使用备用汇率: {}", e.getMessage(), e); + } + + // 如果API调用失败,使用备用汇率 + return getFallbackRate(toCurrency); + } + + @Override + public BigDecimal convertAmount(BigDecimal amount, String fromCurrency, String toCurrency) throws Exception { + if (amount == null) { + throw new IllegalArgumentException("金额不能为空"); + } + + if (!enabled) { + log.warn("汇率服务未启用,使用默认汇率计算"); + BigDecimal rate = getFallbackRate(toCurrency); + // 返回完整精度,不限制小数位 + return amount.multiply(rate); + } + + try { + // 构建请求URL - 使用 CurrencyFreaks Rates Latest Endpoint + String url = buildApiUrl(); + + // 构建请求 + Request request = new Request.Builder() + .url(url) + .get() + .build(); + + // 发送请求 + Response response = OkHttpUtils.newCall(request).execute(); + ResponseBody body = response.body(); + + if (response.isSuccessful() && body != null) { + String responseBody = body.string(); + log.debug("汇率API响应: {}", responseBody); + + // 解析响应 - CurrencyFreaks Rates Latest Endpoint 响应格式 + RatesResponse ratesResponse = objectMapper.readValue(responseBody, RatesResponse.class); + + if (ratesResponse != null && ratesResponse.getRates() != null) { + BigDecimal rate = calculateRate(ratesResponse, fromCurrency, toCurrency); + if (rate != null && rate.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal convertedAmount = amount.multiply(rate); + log.info("成功转换金额 {} {} -> {} {}: {}", + amount, fromCurrency, convertedAmount, toCurrency, rate); + // 返回完整精度,不限制小数位 + return convertedAmount; + } + } + } else { + log.warn("汇率API请求失败,状态码: {}", response.code()); + if (body != null) { + log.warn("响应内容: {}", body.string()); + } + } + } catch (Exception e) { + log.error("转换金额失败,使用备用汇率计算: {}", e.getMessage(), e); + } + + // 如果API调用失败,使用备用汇率计算(不限制小数位) + BigDecimal rate = getFallbackRate(toCurrency); + return amount.multiply(rate); + } + + /** + * 构建API URL - 使用 CurrencyFreaks Rates Latest Endpoint + */ + private String buildApiUrl() { + if (apiUrl.contains("currencyfreaks.com") && apiUrl.contains("/rates/latest")) { + // 使用 CurrencyFreaks Rates Latest Endpoint 格式 + // https://api.currencyfreaks.com/v2.0/rates/latest?apikey=YOUR_APIKEY&base=USD + StringBuilder urlBuilder = new StringBuilder(apiUrl); + urlBuilder.append("?apikey=").append(apikey); + urlBuilder.append("&base=").append(baseCurrency.toUpperCase()); + return urlBuilder.toString(); + } else { + // 兼容其他API格式(向后兼容) + return apiUrl; + } + } + + /** + * 根据汇率响应计算 fromCurrency 到 toCurrency 的汇率 + */ + private BigDecimal calculateRate(RatesResponse ratesResponse, String fromCurrency, String toCurrency) { + String base = ratesResponse.getBase(); + java.util.Map rates = ratesResponse.getRates(); + + if (rates == null || rates.isEmpty()) { + log.warn("汇率响应中 rates 为空"); + return null; + } + + String fromUpper = fromCurrency.toUpperCase(); + String toUpper = toCurrency.toUpperCase(); + String baseUpper = base.toUpperCase(); + + // 如果源货币就是基准货币 + if (baseUpper.equals(fromUpper)) { + String toRateStr = rates.get(toUpper); + if (toRateStr != null) { + try { + return new BigDecimal(toRateStr); + } catch (NumberFormatException e) { + log.error("无法解析目标货币 {} 的汇率: {}", toUpper, toRateStr); + return null; + } + } else { + log.warn("未找到目标货币 {} 的汇率", toUpper); + return null; + } + } + + // 如果目标货币就是基准货币 + if (baseUpper.equals(toUpper)) { + String fromRateStr = rates.get(fromUpper); + if (fromRateStr != null) { + try { + // 如果目标货币是基准货币,汇率是 1 / fromRate + BigDecimal fromRate = new BigDecimal(fromRateStr); + if (fromRate.compareTo(BigDecimal.ZERO) > 0) { + return BigDecimal.ONE.divide(fromRate, 10, RoundingMode.HALF_UP); + } + } catch (NumberFormatException e) { + log.error("无法解析源货币 {} 的汇率: {}", fromUpper, fromRateStr); + return null; + } + } else { + log.warn("未找到源货币 {} 的汇率", fromUpper); + return null; + } + } + + // 两个货币都不是基准货币,需要计算 + String fromRateStr = rates.get(fromUpper); + String toRateStr = rates.get(toUpper); + + if (fromRateStr == null) { + log.warn("未找到源货币 {} 的汇率", fromUpper); + return null; + } + if (toRateStr == null) { + log.warn("未找到目标货币 {} 的汇率", toUpper); + return null; + } + + try { + BigDecimal fromRate = new BigDecimal(fromRateStr); + BigDecimal toRate = new BigDecimal(toRateStr); + + if (fromRate.compareTo(BigDecimal.ZERO) > 0) { + // 汇率 = toRate / fromRate + // 例如:base=USD, from=CNY, to=PHP + // USD->CNY = fromRate, USD->PHP = toRate + // CNY->PHP = toRate / fromRate + return toRate.divide(fromRate, 10, RoundingMode.HALF_UP); + } + } catch (NumberFormatException e) { + log.error("无法解析汇率: from={}, to={}", fromRateStr, toRateStr); + return null; + } + + return null; + } + + /** + * 获取备用汇率(当API调用失败时使用) + */ + private BigDecimal getFallbackRate(String toCurrency) { + // 根据目标货币返回不同的备用汇率 + if ("PHP".equalsIgnoreCase(toCurrency)) { + // PHP比索,使用配置的PHP备用汇率 + log.warn("使用PHP备用汇率: {}", fallbackRatePHP); + return fallbackRatePHP; + } + // 其他货币可以扩展,默认使用PHP的备用汇率 + log.warn("未配置货币 {} 的备用汇率,使用PHP默认汇率 {}", toCurrency, fallbackRatePHP); + return fallbackRatePHP; + } + + /** + * CurrencyFreaks Rates Latest Endpoint 响应对象 + */ + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + static class RatesResponse { + @JsonProperty("date") + private String date; + + @JsonProperty("base") + private String base; + + @JsonProperty("rates") + private java.util.Map rates; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/JinShaService.java b/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/JinShaService.java index 45cbcc0..13f2fdc 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/JinShaService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/JinShaService.java @@ -47,6 +47,8 @@ public class JinShaService implements IJinShaService { private IAiUserService aiUserService; @Autowired private IAiRechargeGiftGearService aiRechargeGiftGearService; + @Autowired + private IExchangeRateService exchangeRateService; @Override public PayResVO jinShaPay(Long gearId) throws Exception { @@ -57,7 +59,8 @@ public class JinShaService implements IJinShaService { throw new ServiceException("The gear position does not exist.", -1); } BigDecimal amount = aiRechargeGiftGear.getRechargeAmount(); - amount = amount.multiply(new BigDecimal(90)); + // 使用汇率服务转换金额(从CNY转换为目标货币,默认INR) + amount = exchangeRateService.convertAmount(amount, "USD", "INR"); JinShaBodyReq req = new JinShaBodyReq(); req.setAppid(appId); diff --git a/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/KaDaService.java b/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/KaDaService.java index 5ed2d92..7f65af4 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/KaDaService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/ai/service/impl/KaDaService.java @@ -54,6 +54,8 @@ public class KaDaService implements IKaDaService { private IAiRechargeService aiRechargeService; @Autowired private IAiRechargeGiftService aiRechargeGiftService; + @Autowired + private IExchangeRateService exchangeRateService; @Override @@ -65,7 +67,8 @@ public class KaDaService implements IKaDaService { throw new ServiceException("The gear position does not exist.", -1); } BigDecimal amount = aiRechargeGiftGear.getRechargeAmount(); - amount = amount.multiply(new BigDecimal(60.00)); + // 使用汇率服务转换金额(从CNY转换为目标货币,默认PHP) + amount = exchangeRateService.convertAmount(amount, "USD", "PHP"); String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8); String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());