perf: 优化首页接口性能,批量并行获取价格
This commit is contained in:
parent
b39044bfe1
commit
65abd50108
@ -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>()
|
var portfolios = _db.Queryable<Portfolio>()
|
||||||
.Where(p => p.UserId == userId)
|
.Where(p => p.UserId == userId)
|
||||||
.ToList();
|
.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>();
|
var result = new List<PortfolioListItem>();
|
||||||
|
|
||||||
foreach (var p in portfolios)
|
foreach (var p in portfolios)
|
||||||
{
|
{
|
||||||
// 获取该组合的所有持仓
|
var positions = allPositions.Where(pos => pos.PortfolioId == p.Id).ToList();
|
||||||
var positions = _db.Queryable<Position>()
|
|
||||||
.Where(pos => pos.PortfolioId == p.Id)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
int positionCount = positions.Count;
|
int positionCount = positions.Count;
|
||||||
|
|
||||||
// 实时计算当前市值和总成本
|
|
||||||
decimal totalValue = 0;
|
decimal totalValue = 0;
|
||||||
decimal totalCost = 0;
|
decimal totalCost = 0;
|
||||||
decimal todayProfit = 0;
|
decimal todayProfit = 0;
|
||||||
@ -196,38 +236,33 @@ public class PortfolioService : IPortfolioService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取实时价格
|
// 从预获取的价格字典中获取
|
||||||
decimal currentPrice = pos.AvgPrice;
|
decimal currentPrice = pos.AvgPrice;
|
||||||
decimal previousClose = 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)
|
if (priceResponse.Price > 0)
|
||||||
{
|
{
|
||||||
currentPrice = priceResponse.Price;
|
currentPrice = priceResponse.Price;
|
||||||
previousClose = priceResponse.PreviousClose > 0 ? priceResponse.PreviousClose : currentPrice;
|
previousClose = priceResponse.PreviousClose > 0 ? priceResponse.PreviousClose : currentPrice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "获取标的 {StockCode} 实时价格失败,使用成本价", pos.StockCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
decimal positionValue = pos.Shares * currentPrice;
|
decimal positionValue = pos.Shares * currentPrice;
|
||||||
decimal positionCost = pos.Shares * pos.AvgPrice;
|
decimal positionCost = pos.Shares * pos.AvgPrice;
|
||||||
decimal positionTodayProfit = previousClose > 0 ? pos.Shares * (currentPrice - previousClose) : 0;
|
decimal positionTodayProfit = previousClose > 0 ? pos.Shares * (currentPrice - previousClose) : 0;
|
||||||
|
|
||||||
// 汇率转换到组合本位币
|
// 汇率转换(汇率服务有缓存,速度很快)
|
||||||
var valueInTarget = _exchangeRateService.ConvertAmountAsync(positionValue, pos.Currency, portfolioCurrency).GetAwaiter().GetResult();
|
var valueInTarget = await _exchangeRateService.ConvertAmountAsync(positionValue, pos.Currency, portfolioCurrency);
|
||||||
var costInTarget = _exchangeRateService.ConvertAmountAsync(positionCost, pos.Currency, portfolioCurrency).GetAwaiter().GetResult();
|
var costInTarget = await _exchangeRateService.ConvertAmountAsync(positionCost, pos.Currency, portfolioCurrency);
|
||||||
var todayProfitInTarget = _exchangeRateService.ConvertAmountAsync(positionTodayProfit, pos.Currency, portfolioCurrency).GetAwaiter().GetResult();
|
var todayProfitInTarget = await _exchangeRateService.ConvertAmountAsync(positionTodayProfit, pos.Currency, portfolioCurrency);
|
||||||
|
|
||||||
totalValue += valueInTarget;
|
totalValue += valueInTarget;
|
||||||
totalCost += costInTarget;
|
totalCost += costInTarget;
|
||||||
todayProfit += todayProfitInTarget;
|
todayProfit += todayProfitInTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算收益率
|
|
||||||
double returnRate = totalCost > 0 ? (double)((totalValue - totalCost) / totalCost * 100) : 0;
|
double returnRate = totalCost > 0 ? (double)((totalValue - totalCost) / totalCost * 100) : 0;
|
||||||
returnRate = Math.Round(returnRate, 2);
|
returnRate = Math.Round(returnRate, 2);
|
||||||
|
|
||||||
@ -253,9 +288,14 @@ public class PortfolioService : IPortfolioService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<PortfolioListItem> GetPortfolios(string userId)
|
||||||
|
{
|
||||||
|
return GetPortfolioListAsync(userId).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TotalAssetsResponse> GetTotalAssetsAsync(string userId)
|
public async Task<TotalAssetsResponse> GetTotalAssetsAsync(string userId)
|
||||||
{
|
{
|
||||||
// 获取用户信息(包含默认本位币)
|
// 获取用户信息
|
||||||
var user = _db.Queryable<User>()
|
var user = _db.Queryable<User>()
|
||||||
.Where(u => u.Id == userId)
|
.Where(u => u.Id == userId)
|
||||||
.First();
|
.First();
|
||||||
@ -267,66 +307,114 @@ public class PortfolioService : IPortfolioService
|
|||||||
|
|
||||||
string targetCurrency = !string.IsNullOrEmpty(user.DefaultCurrency) ? user.DefaultCurrency : "CNY";
|
string targetCurrency = !string.IsNullOrEmpty(user.DefaultCurrency) ? user.DefaultCurrency : "CNY";
|
||||||
_logger.LogInformation("用户 {UserId} 默认本位币: {Currency}", userId, targetCurrency);
|
_logger.LogInformation("用户 {UserId} 默认本位币: {Currency}", userId, targetCurrency);
|
||||||
decimal totalValueInTargetCurrency = 0;
|
|
||||||
decimal totalCostInTargetCurrency = 0;
|
|
||||||
decimal totalTodayProfitInTargetCurrency = 0;
|
|
||||||
|
|
||||||
// 获取用户所有投资组合
|
// 获取所有组合和持仓
|
||||||
var portfolios = _db.Queryable<Portfolio>()
|
var portfolios = _db.Queryable<Portfolio>()
|
||||||
.Where(p => p.UserId == userId)
|
.Where(p => p.UserId == userId)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var portfolio in portfolios)
|
if (!portfolios.Any())
|
||||||
{
|
{
|
||||||
// 获取该组合的所有持仓
|
return new TotalAssetsResponse
|
||||||
var Positions = _db.Queryable<Position>()
|
|
||||||
.Where(pos => pos.PortfolioId == portfolio.Id)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var pos in Positions)
|
|
||||||
{
|
{
|
||||||
if (pos.StockCode == null || pos.Currency == null)
|
TotalValue = 0,
|
||||||
{
|
Currency = targetCurrency,
|
||||||
continue;
|
TodayProfit = 0,
|
||||||
}
|
TodayProfitCurrency = targetCurrency,
|
||||||
|
TotalReturnRate = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 获取实时价格(自动路由数据源),失败则降级使用成本价
|
var portfolioIds = portfolios.Select(p => p.Id).ToList();
|
||||||
decimal CurrentPrice = pos.AvgPrice;
|
var allPositions = _db.Queryable<Position>()
|
||||||
decimal previousClose = pos.AvgPrice;
|
.Where(pos => portfolioIds.Contains(pos.PortfolioId))
|
||||||
try
|
.ToList();
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
decimal currentPositionValue = pos.Shares * CurrentPrice;
|
if (!allPositions.Any())
|
||||||
decimal costPositionValue = pos.Shares * pos.AvgPrice;
|
{
|
||||||
decimal TodayProfit = previousClose > 0 ? pos.Shares * (CurrentPrice - previousClose) : 0;
|
return new TotalAssetsResponse
|
||||||
|
{
|
||||||
|
TotalValue = 0,
|
||||||
|
Currency = targetCurrency,
|
||||||
|
TodayProfit = 0,
|
||||||
|
TodayProfitCurrency = targetCurrency,
|
||||||
|
TotalReturnRate = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 换算到目标本位币
|
// ===== 批量并行获取价格 =====
|
||||||
decimal currentInTarget = await _exchangeRateService.ConvertAmountAsync(currentPositionValue, pos.Currency, targetCurrency);
|
var stockCodes = allPositions
|
||||||
decimal costInTarget = await _exchangeRateService.ConvertAmountAsync(costPositionValue, pos.Currency, targetCurrency);
|
.Where(p => p.StockCode != null)
|
||||||
decimal todayProfitInTarget = await _exchangeRateService.ConvertAmountAsync(TodayProfit, pos.Currency, targetCurrency);
|
.Select(p => p.StockCode!)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
_logger.LogInformation("标的 {StockCode} 换算: {Amount} {From} → {Converted} {To},汇率: {Rate}",
|
var priceDict = new Dictionary<string, MarketPriceResponse>();
|
||||||
pos.StockCode, currentPositionValue, pos.Currency, currentInTarget, targetCurrency,
|
var priceTasks = stockCodes.Select(async code =>
|
||||||
currentPositionValue > 0 ? currentInTarget / currentPositionValue : 0);
|
{
|
||||||
|
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();
|
||||||
|
|
||||||
totalValueInTargetCurrency += currentInTarget;
|
var priceResults = await Task.WhenAll(priceTasks);
|
||||||
totalCostInTargetCurrency += costInTarget;
|
foreach (var (code, price) in priceResults)
|
||||||
totalTodayProfitInTargetCurrency += todayProfitInTarget;
|
{
|
||||||
|
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
|
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)
|
public async Task<PortfolioDetailResponse> GetPortfolioByIdAsync(string id, string userId)
|
||||||
{
|
{
|
||||||
var portfolio = _db.Queryable<Portfolio>()
|
var portfolio = _db.Queryable<Portfolio>()
|
||||||
@ -703,11 +785,6 @@ public class PortfolioService : IPortfolioService
|
|||||||
return Task.FromResult(CreatePortfolio(request, userId));
|
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)
|
public Task<PortfolioDetailResponse> GetPortfolioDetailAsync(string portfolioId, string userId)
|
||||||
{
|
{
|
||||||
return GetPortfolioByIdAsync(portfolioId, userId);
|
return GetPortfolioByIdAsync(portfolioId, userId);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user