using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using System.Net.Http.Json; namespace AssetManager.Infrastructure.Services; /// /// 真实汇率服务,接入 exchangerate-api.com 免费接口 /// public class ExchangeRateService : IExchangeRateService { private readonly HttpClient _httpClient; private readonly IMemoryCache _cache; private readonly ILogger _logger; // 缓存键前缀 private const string CacheKeyPrefix = "ExchangeRate_"; // 缓存过期时间:1小时 private const int CacheExpirationHours = 1; // 基础汇率币种(用USD作为中间货币,减少API调用次数) private const string BaseCurrency = "USD"; public ExchangeRateService( HttpClient httpClient, IMemoryCache cache, ILogger logger) { _httpClient = httpClient; _cache = cache; _logger = logger; } public async Task GetExchangeRateAsync(string fromCurrency, string toCurrency) { _logger.LogInformation("获取汇率: {FromCurrency} -> {ToCurrency}", fromCurrency, toCurrency); // 同币种直接返回1 if (fromCurrency.Equals(toCurrency, StringComparison.OrdinalIgnoreCase)) { return 1.00m; } // 先尝试从缓存获取 string cacheKey = $"{CacheKeyPrefix}{fromCurrency}_{toCurrency}"; if (_cache.TryGetValue(cacheKey, out decimal cachedRate)) { _logger.LogDebug("缓存命中: {Key} = {Rate}", cacheKey, cachedRate); return cachedRate; } try { // 使用免费的开放汇率接口,无需API key var response = await _httpClient.GetFromJsonAsync($"https://open.er-api.com/v6/latest/{BaseCurrency}"); if (response == null || response.Rates == null || response.Result != "success") { _logger.LogError("汇率API调用失败: {Message}", response?.ErrorType ?? "未知错误"); throw new Exception("汇率服务暂时不可用"); } // 转换为USD -> 目标币种的汇率字典 var usdRates = response.Rates; // 计算 from -> to 的汇率:(1 from = x USD) * (1 USD = y to) = x*y to decimal fromToUsd = 1 / usdRates[fromCurrency.ToUpper()]; decimal usdToTo = usdRates[toCurrency.ToUpper()]; decimal rate = fromToUsd * usdToTo; // 存入缓存 var cacheOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(CacheExpirationHours) }; _cache.Set(cacheKey, rate, cacheOptions); // 同时缓存反向汇率 string reverseCacheKey = $"{CacheKeyPrefix}{toCurrency}_{fromCurrency}"; _cache.Set(reverseCacheKey, 1 / rate, cacheOptions); _logger.LogInformation("汇率获取成功: {FromCurrency} -> {ToCurrency} = {Rate}", fromCurrency, toCurrency, rate); return rate; } catch (Exception ex) { _logger.LogError(ex, "获取汇率失败: {FromCurrency} -> {ToCurrency}", fromCurrency, toCurrency); // 失败时降级到Mock汇率 return await GetMockFallbackRate(fromCurrency, toCurrency); } } public async Task ConvertAmountAsync(decimal amount, string fromCurrency, string toCurrency) { _logger.LogInformation("转换金额: {Amount} {FromCurrency} -> {ToCurrency}", amount, fromCurrency, toCurrency); if (fromCurrency == toCurrency) { return amount; } var rate = await GetExchangeRateAsync(fromCurrency, toCurrency); return amount * rate; } /// /// 失败降级到Mock汇率 /// private Task GetMockFallbackRate(string fromCurrency, string toCurrency) { _logger.LogWarning("使用Mock汇率降级: {FromCurrency} -> {ToCurrency}", fromCurrency, toCurrency); var mockRates = new Dictionary { { "CNY-USD", 0.14m }, { "USD-CNY", 7.10m }, { "CNY-HKD", 1.09m }, { "HKD-CNY", 0.92m }, { "USD-HKD", 7.75m }, { "HKD-USD", 0.13m }, { "EUR-USD", 1.08m }, { "USD-EUR", 0.93m }, { "EUR-CNY", 7.70m }, { "CNY-EUR", 0.13m }, { "GBP-USD", 1.27m }, { "USD-GBP", 0.79m }, { "GBP-CNY", 9.00m }, { "CNY-GBP", 0.11m }, { "JPY-USD", 0.0067m }, { "USD-JPY", 149.50m }, { "JPY-CNY", 0.048m }, { "CNY-JPY", 20.90m } }; string key = $"{fromCurrency.ToUpper()}-{toCurrency.ToUpper()}"; if (mockRates.TryGetValue(key, out var rate)) { _logger.LogWarning("Mock汇率命中: {Key} = {Rate},计算结果可能不准确", key, rate); return Task.FromResult(rate); } // 未知货币对,记录严重警告 _logger.LogError("未知货币对,无法提供Mock汇率: {FromCurrency} -> {ToCurrency},返回1.0可能导致计算错误", fromCurrency, toCurrency); // 返回1.0但记录严重警告,调用方应该检查日志 return Task.FromResult(1.00m); } } /// /// 汇率API响应模型(exchangerate-api.com) /// public class ExchangeRateResponse { public string Result { get; set; } = string.Empty; public string ErrorType { get; set; } = string.Empty; public Dictionary ConversionRates { get; set; } = new(); } /// /// 开放汇率API响应模型(er-api.com) /// public class OpenExchangeRateResponse { public string Result { get; set; } = string.Empty; public string ErrorType { get; set; } = string.Empty; public Dictionary Rates { get; set; } = new(); }