AssetManager.API/AssetManager.Infrastructure/Services/ExchangeRateService.cs
OpenClaw Agent 64c1fe60e7 fix: 修复多个金融计算问题
1. CreatePortfolioAsync: 初始建仓交易保存汇率信息
   - 设置 ExchangeRate 和 TotalAmountBase 字段
   - 支持跨币种初始建仓

2. ExchangeRateService: 增强 Mock 汇率降级
   - 扩展支持 EUR、GBP、JPY
   - 未知货币对记录 Error 级别日志

3. PositionItem: 增加 Shares 属性
   - 保留完整精度(解决 Amount int 截断问题)
2026-03-25 05:31:53 +00:00

170 lines
6.0 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 },
{ "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);
}
}
/// <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();
}