diff --git a/AssetManager.Services/PortfolioService.cs b/AssetManager.Services/PortfolioService.cs index dde1b9e..b4ad364 100755 --- a/AssetManager.Services/PortfolioService.cs +++ b/AssetManager.Services/PortfolioService.cs @@ -166,24 +166,64 @@ public class PortfolioService : IPortfolioService }; } - public List GetPortfolios(string userId) + public async Task> GetPortfolioListAsync(string userId) { var portfolios = _db.Queryable() .Where(p => p.UserId == userId) .ToList(); + if (!portfolios.Any()) + { + return new List(); + } + + // ===== 第一步:收集所有需要查询价格的股票代码 ===== + var portfolioIds = portfolios.Select(p => p.Id).ToList(); + var allPositions = _db.Queryable() + .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(); + 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(); foreach (var p in portfolios) { - // 获取该组合的所有持仓 - var positions = _db.Queryable() - .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 GetPortfolios(string userId) + { + return GetPortfolioListAsync(userId).GetAwaiter().GetResult(); + } + public async Task GetTotalAssetsAsync(string userId) { - // 获取用户信息(包含默认本位币) + // 获取用户信息 var user = _db.Queryable() .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() .Where(p => p.UserId == userId) .ToList(); - foreach (var portfolio in portfolios) + if (!portfolios.Any()) { - // 获取该组合的所有持仓 - var Positions = _db.Queryable() - .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() + .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(); + 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 GetPortfolioByIdAsync(string id, string userId) { var portfolio = _db.Queryable() @@ -703,11 +785,6 @@ public class PortfolioService : IPortfolioService return Task.FromResult(CreatePortfolio(request, userId)); } - public Task> GetPortfolioListAsync(string userId) - { - return Task.FromResult(GetPortfolios(userId)); - } - public Task GetPortfolioDetailAsync(string portfolioId, string userId) { return GetPortfolioByIdAsync(portfolioId, userId);