AssetManager.API/AssetManager.Infrastructure/Services/ExchangeRateService.cs

136 lines
4.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System.Net.Http.Json;
namespace AssetManager.Infrastructure.Services;
/// <summary>
/// 真实汇率服务,接入 exchangerate-api.com 免费接口
/// </summary>
public class ExchangeRateService : IExchangeRateService
{
private readonly HttpClient _httpClient;
private readonly IMemoryCache _cache;
private readonly ILogger<ExchangeRateService> _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<ExchangeRateService> logger)
{
_httpClient = httpClient;
_cache = cache;
_logger = logger;
}
public async Task<decimal> 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<ExchangeRateResponse>($"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<decimal> 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;
}
/// <summary>
/// 失败降级到Mock汇率
/// </summary>
private Task<decimal> GetMockFallbackRate(string fromCurrency, string toCurrency)
{
_logger.LogWarning("使用Mock汇率降级: {FromCurrency} -> {ToCurrency}", fromCurrency, toCurrency);
var mockRates = new Dictionary<string, decimal>
{
{ "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);
}
}
/// <summary>
/// 汇率API响应模型
/// </summary>
public class ExchangeRateResponse
{
public string Result { get; set; } = string.Empty;
public string ErrorType { get; set; } = string.Empty;
public Dictionary<string, decimal> ConversionRates { get; set; } = new();
}