Files
BI/server/DongfangHydro.Dashboard.Api/Services/DashboardQueryService.cs
T

383 lines
14 KiB
C#

using DongfangHydro.Dashboard.Api.Contracts;
using DongfangHydro.Dashboard.Api.Data;
using DongfangHydro.Dashboard.Api.Domain;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
namespace DongfangHydro.Dashboard.Api.Services;
public sealed class DashboardQueryService(DashboardDbContext dbContext)
{
public async Task<PagedResult<MaterialQuotaDto>> GetMaterialQuotasAsync(
Guid orderId,
string? category,
string? keyword,
int page,
int pageSize,
CancellationToken cancellationToken)
{
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 1, 200);
var query = dbContext.MaterialQuotas.AsNoTracking().Where(item => item.OrderId == orderId);
if (!string.IsNullOrWhiteSpace(category))
{
var normalizedCategory = category.Trim();
query = query.Where(item => item.Category == normalizedCategory);
}
if (!string.IsNullOrWhiteSpace(keyword))
{
var normalizedKeyword = keyword.Trim();
query = query.Where(item =>
item.MaterialCode.Contains(normalizedKeyword)
|| item.MaterialName.Contains(normalizedKeyword)
|| item.Specification.Contains(normalizedKeyword));
}
var total = await query.CountAsync(cancellationToken);
var rows = await query
.OrderBy(item => item.Category)
.ThenBy(item => item.SourceRow)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
var items = rows.Select(item => new MaterialQuotaDto(
item.Id,
item.OrderId,
item.Category,
item.SourceSequence,
item.MaterialCode,
item.MaterialName,
item.Specification,
item.Unit,
item.Quantity,
item.NetWeight,
item.ConsumptionQuota,
item.QuantityText,
item.NetWeightText,
item.ConsumptionQuotaText,
item.Brand,
item.Remark,
item.SourceSheet,
item.SourceRow)).ToList();
return new PagedResult<MaterialQuotaDto>(items, total, page, pageSize);
}
public async Task<PagedResult<OrderSummaryDto>> GetOrdersAsync(
string? keyword,
string? status,
Guid? lineId,
int page,
int pageSize,
CancellationToken cancellationToken)
{
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 1, 100);
var query = dbContext.ProductionOrders.AsNoTracking();
if (!string.IsNullOrWhiteSpace(keyword))
{
var normalized = keyword.Trim();
query = query.Where(order =>
order.Code.Contains(normalized)
|| order.ProductName.Contains(normalized)
|| order.LineName.Contains(normalized)
|| order.Owner.Contains(normalized));
}
if (!string.IsNullOrWhiteSpace(status))
{
query = query.Where(order => order.Status == status);
}
if (lineId.HasValue)
{
query = query.Where(order => order.LineId == lineId);
}
var total = await query.CountAsync(cancellationToken);
var orders = await query
.OrderBy(order => order.Code)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
var items = await BuildOrderSummariesAsync(orders, cancellationToken);
return new PagedResult<OrderSummaryDto>(items, total, page, pageSize);
}
public async Task<OrderSummaryDto?> GetOrderAsync(Guid orderId, CancellationToken cancellationToken)
{
var order = await dbContext.ProductionOrders
.AsNoTracking()
.SingleOrDefaultAsync(item => item.Id == orderId, cancellationToken);
if (order is null)
{
return null;
}
return (await BuildOrderSummariesAsync([order], cancellationToken)).SingleOrDefault();
}
public async Task<ProductionNodeDto?> GetTreeAsync(Guid orderId, CancellationToken cancellationToken)
{
var nodes = await dbContext.ProductionNodes
.AsNoTracking()
.Where(node => node.OrderId == orderId)
.OrderBy(node => node.Level)
.ThenBy(node => node.SortOrder)
.ToListAsync(cancellationToken);
return DashboardMapper.BuildTree(nodes);
}
public async Task<IReadOnlyList<TrendPointDto>> GetTrendAsync(
Guid orderId,
int limit,
CancellationToken cancellationToken)
{
var points = await dbContext.ProductionTrendPoints
.AsNoTracking()
.Where(point => point.OrderId == orderId)
.OrderByDescending(point => point.SampleTime)
.Take(Math.Clamp(limit, 1, 100))
.ToListAsync(cancellationToken);
return points.OrderBy(point => point.SampleTime).Select(DashboardMapper.MapTrend).ToList();
}
public async Task<IReadOnlyList<RiskEventDto>> GetRiskEventsAsync(
Guid? orderId,
int limit,
CancellationToken cancellationToken)
{
var query = dbContext.RiskEvents.AsNoTracking();
if (orderId.HasValue)
{
query = query.Where(riskEvent => riskEvent.OrderId == orderId);
}
var events = await query
.OrderByDescending(riskEvent => riskEvent.OccurredAt)
.Take(Math.Clamp(limit, 1, 100))
.ToListAsync(cancellationToken);
return events.Select(DashboardMapper.MapRiskEvent).ToList();
}
public async Task<DashboardOverviewDto> GetOverviewAsync(CancellationToken cancellationToken)
{
var orders = await dbContext.ProductionOrders.AsNoTracking().ToListAsync(cancellationToken);
var totalRequired = orders.Sum(order => order.RequiredQty);
var totalCompleted = orders.Sum(order => order.CompletedQty);
return new DashboardOverviewDto(
orders.Count,
totalRequired,
totalCompleted,
totalRequired == 0 ? 0 : Math.Round(totalCompleted * 100d / totalRequired, 1),
orders.Count(order => order.DelayDays > 0 || order.Status is "delayed" or "blocked"),
orders.Count(order => order.RiskLevel == "critical"),
orders.Count(order => order.RiskLevel == "warning"),
orders.Count == 0 ? 0 : Math.Round(orders.Average(order => order.PlanAchievement), 1),
DateTimeOffset.UtcNow);
}
public async Task<IReadOnlyList<DelayTopItemDto>> GetDelayTopAsync(
int limit,
CancellationToken cancellationToken)
{
var orders = await dbContext.ProductionOrders
.AsNoTracking()
.Where(order => order.DelayDays > 0 || order.Status == "delayed" || order.Status == "blocked")
.OrderByDescending(order => order.DelayDays)
.ThenByDescending(order => order.RiskLevel == "critical")
.Take(Math.Clamp(limit, 1, 50))
.ToListAsync(cancellationToken);
var orderIds = orders.Select(order => order.Id).ToArray();
var recentEvents = await LoadRecentRiskEventsAsync(orderIds, 1, cancellationToken);
var result = new List<DelayTopItemDto>(orders.Count);
for (var index = 0; index < orders.Count; index++)
{
var order = orders[index];
var delayReason = recentEvents.FirstOrDefault(riskEvent => riskEvent.OrderId == order.Id)?.Message;
result.Add(new DelayTopItemDto(
index + 1,
order.Id,
order.Code,
order.ProductName,
order.DelayDays,
delayReason ?? "计划节拍低于目标",
order.RiskLevel));
}
return result;
}
private async Task<IReadOnlyList<OrderSummaryDto>> BuildOrderSummariesAsync(
IReadOnlyList<ProductionOrder> orders,
CancellationToken cancellationToken)
{
if (orders.Count == 0)
{
return [];
}
var orderIds = orders.Select(order => order.Id).ToList();
var nodes = await dbContext.ProductionNodes
.AsNoTracking()
.Where(node => orderIds.Contains(node.OrderId))
.OrderBy(node => node.Level)
.ThenBy(node => node.SortOrder)
.ToListAsync(cancellationToken);
var trend = await LoadRecentTrendAsync(orderIds, 12, cancellationToken);
var events = await LoadRecentRiskEventsAsync(orderIds, 8, cancellationToken);
var result = new List<OrderSummaryDto>(orders.Count);
foreach (var order in orders)
{
var root = DashboardMapper.BuildTree(nodes.Where(node => node.OrderId == order.Id).ToList());
if (root is null)
{
continue;
}
var orderTrend = trend.Where(point => point.OrderId == order.Id);
var orderEvents = events
.Where(riskEvent => riskEvent.OrderId == order.Id)
.OrderByDescending(riskEvent => riskEvent.OccurredAt);
result.Add(DashboardMapper.MapOrder(
order,
root,
orderTrend.OrderBy(point => point.SampleTime).Select(DashboardMapper.MapTrend).ToList(),
orderEvents.Select(DashboardMapper.MapRiskEvent).ToList()));
}
return result;
}
private async Task<List<ProductionTrendPoint>> LoadRecentTrendAsync(
IReadOnlyCollection<Guid> orderIds,
int perOrderLimit,
CancellationToken cancellationToken)
{
if (orderIds.Count == 0)
{
return [];
}
if (dbContext.Database.IsSqlServer())
{
return await BuildRecentTrendQuery(dbContext, orderIds, perOrderLimit)
.AsNoTracking()
.ToListAsync(cancellationToken);
}
var points = await dbContext.ProductionTrendPoints
.AsNoTracking()
.Where(point => orderIds.Contains(point.OrderId))
.ToListAsync(cancellationToken);
return points
.GroupBy(point => point.OrderId)
.SelectMany(group => group.OrderByDescending(point => point.SampleTime).Take(perOrderLimit))
.ToList();
}
private async Task<List<RiskEvent>> LoadRecentRiskEventsAsync(
IReadOnlyCollection<Guid> orderIds,
int perOrderLimit,
CancellationToken cancellationToken)
{
if (orderIds.Count == 0)
{
return [];
}
if (dbContext.Database.IsSqlServer())
{
return await BuildRecentRiskEventQuery(dbContext, orderIds, perOrderLimit)
.AsNoTracking()
.ToListAsync(cancellationToken);
}
var events = await dbContext.RiskEvents
.AsNoTracking()
.Where(riskEvent => orderIds.Contains(riskEvent.OrderId))
.ToListAsync(cancellationToken);
return events
.GroupBy(riskEvent => riskEvent.OrderId)
.SelectMany(group => group.OrderByDescending(riskEvent => riskEvent.OccurredAt).Take(perOrderLimit))
.ToList();
}
public static IQueryable<ProductionTrendPoint> BuildRecentTrendQuery(
DashboardDbContext dbContext,
IReadOnlyCollection<Guid> orderIds,
int perOrderLimit)
{
if (orderIds.Count == 0)
{
return dbContext.ProductionTrendPoints.Where(_ => false);
}
var (placeholders, parameters) = BuildOrderIdParameters(orderIds);
parameters.Add(new SqlParameter("@perOrderLimit", Math.Max(1, perOrderLimit)));
var sql = $$"""
SELECT [ranked].[Id], [ranked].[OrderId], [ranked].[SampleTime], [ranked].[Completion],
[ranked].[Risk], [ranked].[PlannedQty], [ranked].[ActualQty], [ranked].[Achievement]
FROM (
SELECT [point].*, ROW_NUMBER() OVER (
PARTITION BY [point].[OrderId]
ORDER BY [point].[SampleTime] DESC, [point].[Id] DESC
) AS [row_number]
FROM [ProductionTrendPoints] AS [point]
WHERE [point].[OrderId] IN ({{placeholders}})
) AS [ranked]
WHERE [ranked].[row_number] <= @perOrderLimit
""";
return dbContext.ProductionTrendPoints.FromSqlRaw(sql, [.. parameters]);
}
private static IQueryable<RiskEvent> BuildRecentRiskEventQuery(
DashboardDbContext dbContext,
IReadOnlyCollection<Guid> orderIds,
int perOrderLimit)
{
if (orderIds.Count == 0)
{
return dbContext.RiskEvents.Where(_ => false);
}
var (placeholders, parameters) = BuildOrderIdParameters(orderIds);
parameters.Add(new SqlParameter("@perOrderLimit", Math.Max(1, perOrderLimit)));
var sql = $$"""
SELECT [ranked].[Id], [ranked].[OrderId], [ranked].[NodeId], [ranked].[OrderCode],
[ranked].[NodeName], [ranked].[RiskLevel], [ranked].[Message],
[ranked].[HandlingStatus], [ranked].[OccurredAt]
FROM (
SELECT [event].*, ROW_NUMBER() OVER (
PARTITION BY [event].[OrderId]
ORDER BY [event].[OccurredAt] DESC, [event].[Id] DESC
) AS [row_number]
FROM [RiskEvents] AS [event]
WHERE [event].[OrderId] IN ({{placeholders}})
) AS [ranked]
WHERE [ranked].[row_number] <= @perOrderLimit
""";
return dbContext.RiskEvents.FromSqlRaw(sql, [.. parameters]);
}
private static (string Placeholders, List<object> Parameters) BuildOrderIdParameters(
IReadOnlyCollection<Guid> orderIds)
{
var parameters = orderIds
.Select((orderId, index) => (object)new SqlParameter($"@orderId{index}", orderId))
.ToList();
var placeholders = string.Join(", ", Enumerable.Range(0, orderIds.Count).Select(index => $"@orderId{index}"));
return (placeholders, parameters);
}
}