AssetManager.API/AssetManager.Infrastructure/Services/ExchangeRateService.cs
OpenClaw Agent 1977dd609d fix: 请求收益曲线时自动回填历史数据
- GetNavHistoryAsync现在会自动检查是否有历史数据
- 无历史数据时自动调用BackfillNavHistoryInternalAsync
- 拆分内部回填方法,避免重复验证权限
2026-03-13 16:21:31 +00:00

146 lines
5.1 KiB
C#
Executable File
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
{
// 使用免费的开放汇率接口无需API key
var response = await _httpClient.GetFromJsonAsync<OpenExchangeRateResponse>($"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<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响应模型exchangerate-api.com
/// </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();
}
/// <summary>
/// 开放汇率API响应模型er-api.com
/// </summary>
public class OpenExchangeRateResponse
{
public string Result { get; set; } = string.Empty;
public string ErrorType { get; set; } = string.Empty;
public Dictionary<string, decimal> Rates { get; set; } = new();
}