perf: 优化首页接口性能,批量并行获取价格

This commit is contained in:
OpenClaw Agent 2026-03-17 08:18:56 +00:00
parent b39044bfe1
commit 65abd50108

View File

@ -166,24 +166,64 @@ public class PortfolioService : IPortfolioService
};
}
public List<PortfolioListItem> GetPortfolios(string userId)
public async Task<List<PortfolioListItem>> GetPortfolioListAsync(string userId)
{
var portfolios = _db.Queryable<Portfolio>()
.Where(p => p.UserId == userId)
.ToList();
if (!portfolios.Any())
{
return new List<PortfolioListItem>();
}
// ===== 第一步:收集所有需要查询价格的股票代码 =====
var portfolioIds = portfolios.Select(p => p.Id).ToList();
var allPositions = _db.Queryable<Position>()
.Where(pos => portfolioIds.Contains(pos.PortfolioId))
.ToList();
// 去重获取所有股票代码
var stockCodes = allPositions
.Where(p => p.StockCode != null)
.Select(p => p.StockCode!)
.Distinct()
.ToList();
// ===== 第二步:批量并行获取价格(利用缓存) =====
var priceDict = new Dictionary<string, MarketPriceResponse>();
var priceTasks = stockCodes.Select(async code =>
{
try
{
var pos = allPositions.First(p => p.StockCode == code);
var price = await _marketDataService.GetPriceAsync(code, pos.AssetType ?? "Stock");
return (code, price);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "批量获取价格失败: {Code}", code);
return (code, null);
}
}).ToList();
var priceResults = await Task.WhenAll(priceTasks);
foreach (var (code, price) in priceResults)
{
if (price != null)
{
priceDict[code] = price;
}
}
// ===== 第三步:计算每个组合的数据 =====
var result = new List<PortfolioListItem>();
foreach (var p in portfolios)
{
// 获取该组合的所有持仓
var positions = _db.Queryable<Position>()
.Where(pos => pos.PortfolioId == p.Id)
.ToList();
var positions = allPositions.Where(pos => pos.PortfolioId == p.Id).ToList();
int positionCount = positions.Count;
// 实时计算当前市值和总成本
decimal totalValue = 0;
decimal totalCost = 0;
decimal todayProfit = 0;
@ -196,38 +236,33 @@ public class PortfolioService : IPortfolioService
continue;
}
// 获取实时价格
// 从预获取的价格字典中获取
decimal currentPrice = pos.AvgPrice;
decimal previousClose = pos.AvgPrice;
try
if (priceDict.TryGetValue(pos.StockCode, out var priceResponse))
{
var priceResponse = _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock").GetAwaiter().GetResult();
if (priceResponse.Price > 0)
{
currentPrice = priceResponse.Price;
previousClose = priceResponse.PreviousClose > 0 ? priceResponse.PreviousClose : currentPrice;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "获取标的 {StockCode} 实时价格失败,使用成本价", pos.StockCode);
}
decimal positionValue = pos.Shares * currentPrice;
decimal positionCost = pos.Shares * pos.AvgPrice;
decimal positionTodayProfit = previousClose > 0 ? pos.Shares * (currentPrice - previousClose) : 0;
// 汇率转换到组合本位币
var valueInTarget = _exchangeRateService.ConvertAmountAsync(positionValue, pos.Currency, portfolioCurrency).GetAwaiter().GetResult();
var costInTarget = _exchangeRateService.ConvertAmountAsync(positionCost, pos.Currency, portfolioCurrency).GetAwaiter().GetResult();
var todayProfitInTarget = _exchangeRateService.ConvertAmountAsync(positionTodayProfit, pos.Currency, portfolioCurrency).GetAwaiter().GetResult();
// 汇率转换(汇率服务有缓存,速度很快)
var valueInTarget = await _exchangeRateService.ConvertAmountAsync(positionValue, pos.Currency, portfolioCurrency);
var costInTarget = await _exchangeRateService.ConvertAmountAsync(positionCost, pos.Currency, portfolioCurrency);
var todayProfitInTarget = await _exchangeRateService.ConvertAmountAsync(positionTodayProfit, pos.Currency, portfolioCurrency);
totalValue += valueInTarget;
totalCost += costInTarget;
todayProfit += todayProfitInTarget;
}
// 计算收益率
double returnRate = totalCost > 0 ? (double)((totalValue - totalCost) / totalCost * 100) : 0;
returnRate = Math.Round(returnRate, 2);
@ -253,9 +288,14 @@ public class PortfolioService : IPortfolioService
return result;
}
public List<PortfolioListItem> GetPortfolios(string userId)
{
return GetPortfolioListAsync(userId).GetAwaiter().GetResult();
}
public async Task<TotalAssetsResponse> GetTotalAssetsAsync(string userId)
{
// 获取用户信息(包含默认本位币)
// 获取用户信息
var user = _db.Queryable<User>()
.Where(u => u.Id == userId)
.First();
@ -267,66 +307,114 @@ public class PortfolioService : IPortfolioService
string targetCurrency = !string.IsNullOrEmpty(user.DefaultCurrency) ? user.DefaultCurrency : "CNY";
_logger.LogInformation("用户 {UserId} 默认本位币: {Currency}", userId, targetCurrency);
decimal totalValueInTargetCurrency = 0;
decimal totalCostInTargetCurrency = 0;
decimal totalTodayProfitInTargetCurrency = 0;
// 获取用户所有投资组合
// 获取所有组合和持仓
var portfolios = _db.Queryable<Portfolio>()
.Where(p => p.UserId == userId)
.ToList();
foreach (var portfolio in portfolios)
if (!portfolios.Any())
{
// 获取该组合的所有持仓
var Positions = _db.Queryable<Position>()
.Where(pos => pos.PortfolioId == portfolio.Id)
.ToList();
foreach (var pos in Positions)
return new TotalAssetsResponse
{
if (pos.StockCode == null || pos.Currency == null)
{
continue;
}
TotalValue = 0,
Currency = targetCurrency,
TodayProfit = 0,
TodayProfitCurrency = targetCurrency,
TotalReturnRate = 0
};
}
// 获取实时价格(自动路由数据源),失败则降级使用成本价
decimal CurrentPrice = pos.AvgPrice;
decimal previousClose = pos.AvgPrice;
try
{
var priceResponse = await _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock");
if (priceResponse.Price > 0)
{
CurrentPrice = priceResponse.Price;
previousClose = priceResponse.PreviousClose > 0 ? priceResponse.PreviousClose : CurrentPrice;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "获取标的 {StockCode} 实时价格失败,使用成本价作为当前价", pos.StockCode);
}
var portfolioIds = portfolios.Select(p => p.Id).ToList();
var allPositions = _db.Queryable<Position>()
.Where(pos => portfolioIds.Contains(pos.PortfolioId))
.ToList();
decimal currentPositionValue = pos.Shares * CurrentPrice;
decimal costPositionValue = pos.Shares * pos.AvgPrice;
decimal TodayProfit = previousClose > 0 ? pos.Shares * (CurrentPrice - previousClose) : 0;
if (!allPositions.Any())
{
return new TotalAssetsResponse
{
TotalValue = 0,
Currency = targetCurrency,
TodayProfit = 0,
TodayProfitCurrency = targetCurrency,
TotalReturnRate = 0
};
}
// 换算到目标本位币
decimal currentInTarget = await _exchangeRateService.ConvertAmountAsync(currentPositionValue, pos.Currency, targetCurrency);
decimal costInTarget = await _exchangeRateService.ConvertAmountAsync(costPositionValue, pos.Currency, targetCurrency);
decimal todayProfitInTarget = await _exchangeRateService.ConvertAmountAsync(TodayProfit, pos.Currency, targetCurrency);
_logger.LogInformation("标的 {StockCode} 换算: {Amount} {From} → {Converted} {To},汇率: {Rate}",
pos.StockCode, currentPositionValue, pos.Currency, currentInTarget, targetCurrency,
currentPositionValue > 0 ? currentInTarget / currentPositionValue : 0);
// ===== 批量并行获取价格 =====
var stockCodes = allPositions
.Where(p => p.StockCode != null)
.Select(p => p.StockCode!)
.Distinct()
.ToList();
totalValueInTargetCurrency += currentInTarget;
totalCostInTargetCurrency += costInTarget;
totalTodayProfitInTargetCurrency += todayProfitInTarget;
var priceDict = new Dictionary<string, MarketPriceResponse>();
var priceTasks = stockCodes.Select(async code =>
{
try
{
var pos = allPositions.First(p => p.StockCode == code);
var price = await _marketDataService.GetPriceAsync(code, pos.AssetType ?? "Stock");
return (code, price);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "批量获取价格失败: {Code}", code);
return (code, null);
}
}).ToList();
var priceResults = await Task.WhenAll(priceTasks);
foreach (var (code, price) in priceResults)
{
if (price != null)
{
priceDict[code] = price;
}
}
double totalReturnRate = totalCostInTargetCurrency > 0 ? (double)((totalValueInTargetCurrency - totalCostInTargetCurrency) / totalCostInTargetCurrency * 100) : 0;
// ===== 计算总资产 =====
decimal totalValueInTargetCurrency = 0;
decimal totalCostInTargetCurrency = 0;
decimal totalTodayProfitInTargetCurrency = 0;
foreach (var pos in allPositions)
{
if (pos.StockCode == null || pos.Currency == null)
{
continue;
}
decimal currentPrice = pos.AvgPrice;
decimal previousClose = pos.AvgPrice;
if (priceDict.TryGetValue(pos.StockCode, out var priceResponse))
{
if (priceResponse.Price > 0)
{
currentPrice = priceResponse.Price;
previousClose = priceResponse.PreviousClose > 0 ? priceResponse.PreviousClose : currentPrice;
}
}
decimal positionValue = pos.Shares * currentPrice;
decimal costValue = pos.Shares * pos.AvgPrice;
decimal todayProfit = previousClose > 0 ? pos.Shares * (currentPrice - previousClose) : 0;
// 汇率转换
var currentInTarget = await _exchangeRateService.ConvertAmountAsync(positionValue, pos.Currency, targetCurrency);
var costInTarget = await _exchangeRateService.ConvertAmountAsync(costValue, pos.Currency, targetCurrency);
var todayProfitInTarget = await _exchangeRateService.ConvertAmountAsync(todayProfit, pos.Currency, targetCurrency);
totalValueInTargetCurrency += currentInTarget;
totalCostInTargetCurrency += costInTarget;
totalTodayProfitInTargetCurrency += todayProfitInTarget;
}
double totalReturnRate = totalCostInTargetCurrency > 0
? (double)((totalValueInTargetCurrency - totalCostInTargetCurrency) / totalCostInTargetCurrency * 100)
: 0;
return new TotalAssetsResponse
{
@ -338,12 +426,6 @@ public class PortfolioService : IPortfolioService
};
}
// 保留同步方法作为兼容
public TotalAssetsResponse GetTotalAssets(string userId)
{
return GetTotalAssetsAsync(userId).GetAwaiter().GetResult();
}
public async Task<PortfolioDetailResponse> GetPortfolioByIdAsync(string id, string userId)
{
var portfolio = _db.Queryable<Portfolio>()
@ -703,11 +785,6 @@ public class PortfolioService : IPortfolioService
return Task.FromResult(CreatePortfolio(request, userId));
}
public Task<List<PortfolioListItem>> GetPortfolioListAsync(string userId)
{
return Task.FromResult(GetPortfolios(userId));
}
public Task<PortfolioDetailResponse> GetPortfolioDetailAsync(string portfolioId, string userId)
{
return GetPortfolioByIdAsync(portfolioId, userId);