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
{
// 以USD为基础货币获取所有汇率
var response = await _httpClient.GetFromJsonAsync($"https://v6.exchangerate-api.com/v6/4a8a42b4b9a3c4d5e6f7a8b9/latest/{BaseCurrency}");
if (response == null || response.ConversionRates == null || response.Result != "success")
{
_logger.LogError("汇率API调用失败: {Message}", response?.ErrorType ?? "未知错误");
throw new Exception("汇率服务暂时不可用");
}
// 转换为USD -> 目标币种的汇率字典
var usdRates = response.ConversionRates;
// 计算 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 }
};
string key = $"{fromCurrency.ToUpper()}-{toCurrency.ToUpper()}";
return Task.FromResult(mockRates.TryGetValue(key, out var rate) ? rate : 1.00m);
}
}
///
/// 汇率API响应模型
///
public class ExchangeRateResponse
{
public string Result { get; set; } = string.Empty;
public string ErrorType { get; set; } = string.Empty;
public Dictionary ConversionRates { get; set; } = new();
}