383 lines
14 KiB
C#
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);
|
|
}
|
|
}
|