using System.Text.Json; using Admin.NET.Plugin.AiDOP.Dto.S8.OrderFlow; using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Entity.S8.OrderFlow; namespace Admin.NET.Plugin.AiDOP.Service.S8.OrderFlow; /// /// ORDER-FLOW-S8-INTEGRATED-DOMAIN-RESET-1 t3b:S8 订单执行链路只读 service skeleton。 /// 仅 GetOrders / GetOrder / GetChain 三个方法;procurement-pivot / aggregate / related-exceptions 延后切片。 /// 任何业务真值(订单总数 / 平均时长 / YY 真值矩阵等)只能来自 DB,本文件零硬编码。 /// 异常计数:从 ado_s8_exception 实时聚合(SourceObjectType=SALES_ORDER + RelatedObjectCode in order_codes + IsDeleted=0)。 /// public class S8OrderFlowService : ITransient { private const string ExceptionSourceObjectType = "SALES_ORDER"; private const string ScopeBaselinePpt = "BASELINE_PPT"; private const string ScopeCurrentFiltered = "CURRENT_FILTERED"; private const string BaselineSnapshotCode = "CHAIN_AGGREGATE_BASELINE"; private const string PivotTotalCode = "TOTAL"; // ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:异常类型 → 末端协同对象 映射常量。 // 这些 type_code 均来自 ado_s8_exception_type seed 中 enabled=true、monitoring_category_key=FINAL_ASSEMBLY_DELIVERY 的子集; // 不新增异常类型、不依赖业务执行表。空数组对象(如 ASSEMBLY_COMPLETION / 包装材料准备)在前端展示 "--"。 private const string FinalAssemblyObjectGoodsHandover = "GOODS_HANDOVER"; private const string FinalAssemblyObjectShipmentConfirmation = "SHIPMENT_CONFIRMATION"; private const string FinalAssemblyObjectShippingPlan = "SHIPPING_PLAN"; private static readonly string[] GoodsHandoverExceptionTypes = { "PENDING_SHIPMENT" }; private static readonly string[] ShipmentConfirmationExceptionTypes = { "DELIVERY_DELAY", "SHIPMENT_ABNORMAL", "DELIVERY_DELAY_WARNING", "FINISHED_GOODS_PENDING_SHIPMENT", }; private static readonly string[] ShippingPlanExceptionTypes = { "FINISHED_GOODS_PENDING_SHIPMENT" }; private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true, }; private readonly SqlSugarRepository _orderRep; private readonly SqlSugarRepository _stageRep; private readonly SqlSugarRepository _substepRep; private readonly SqlSugarRepository _unitRep; private readonly SqlSugarRepository _snapshotRep; private readonly SqlSugarRepository _pivotRep; private readonly SqlSugarRepository _exceptionRep; private readonly SqlSugarRepository _pddRep; public S8OrderFlowService( SqlSugarRepository orderRep, SqlSugarRepository stageRep, SqlSugarRepository substepRep, SqlSugarRepository unitRep, SqlSugarRepository snapshotRep, SqlSugarRepository pivotRep, SqlSugarRepository exceptionRep, SqlSugarRepository pddRep) { _orderRep = orderRep; _stageRep = stageRep; _substepRep = substepRep; _unitRep = unitRep; _snapshotRep = snapshotRep; _pivotRep = pivotRep; _exceptionRep = exceptionRep; _pddRep = pddRep; } /// 订单档案列表(无分页)。当前 baseline 20 单。 public async Task> GetOrdersAsync(AdoS8OrderFlowOrderQueryDto query) { var tenantId = query.TenantId ?? 1; var factoryId = query.FactoryId ?? 1; var orders = await _orderRep.AsQueryable() .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId && !o.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(query.Keyword), o => o.OrderCode.Contains(query.Keyword!) || o.ProductName.Contains(query.Keyword!) || o.CustomerName.Contains(query.Keyword!)) .WhereIF(!string.IsNullOrWhiteSpace(query.CurrentFlowCode), o => o.CurrentOrderFlowCode == query.CurrentFlowCode!) .WhereIF(!string.IsNullOrWhiteSpace(query.CustomerCode), o => o.CustomerCode == query.CustomerCode!) .WhereIF(!string.IsNullOrWhiteSpace(query.ProductLine), o => o.ProductLine == query.ProductLine!) .WhereIF(!string.IsNullOrWhiteSpace(query.Region), o => o.Region == query.Region!) .WhereIF(!string.IsNullOrWhiteSpace(query.WorkflowStatus), o => o.WorkflowStatus == query.WorkflowStatus!) .WhereIF(!string.IsNullOrWhiteSpace(query.ScenarioCode), o => o.ScenarioCode == query.ScenarioCode!) .OrderBy(o => o.OrderCode) .ToListAsync(); if (orders.Count == 0) return new List(); var orderCodes = orders.Select(o => o.OrderCode).Distinct().ToList(); var orderIds = orders.Select(o => o.Id).ToList(); var currentStages = await _stageRep.AsQueryable() .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted) .Where(s => orderIds.Contains(s.OrderId)) .ToListAsync(); var currentStageMap = currentStages .GroupBy(s => s.OrderId) .ToDictionary(g => g.Key, g => g.ToList()); var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, orderCodes); return orders.Select(o => MapOrderListItem(o, currentStageMap.GetValueOrDefault(o.Id), exceptionCounts)).ToList(); } /// 单订单详情(不含 substep/unit)。 public async Task GetOrderAsync(string orderCode, long tenantId = 1, long factoryId = 1) { if (string.IsNullOrWhiteSpace(orderCode)) return null; var order = await _orderRep.AsQueryable() .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId && !o.IsDeleted && o.OrderCode == orderCode) .FirstAsync(); if (order == null) return null; var stages = await _stageRep.AsQueryable() .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted && s.OrderId == order.Id) .OrderBy(s => s.SortNo) .ToListAsync(); var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, new List { orderCode }); var listItem = MapOrderListItem(order, stages, exceptionCounts); return BuildDetail(listItem, stages, substepsByFlow: null, unitsBySubstepId: null); } /// 订单链路视图:详情 + lifecycle + L2 substeps + L3 units。procurementPivot 留到 t3c。 public async Task GetChainAsync(string orderCode, long tenantId = 1, long factoryId = 1) { if (string.IsNullOrWhiteSpace(orderCode)) return null; var order = await _orderRep.AsQueryable() .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId && !o.IsDeleted && o.OrderCode == orderCode) .FirstAsync(); if (order == null) return null; var stages = await _stageRep.AsQueryable() .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted && s.OrderId == order.Id) .OrderBy(s => s.SortNo) .ToListAsync(); var substeps = await _substepRep.AsQueryable() .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted && x.OrderId == order.Id) .OrderBy(x => x.SortNo) .ToListAsync(); var substepsByFlow = substeps .GroupBy(x => x.OrderFlowCode) .ToDictionary(g => g.Key, g => g.ToList()); var substepIds = substeps.Select(x => x.Id).ToList(); var units = substepIds.Count == 0 ? new List() : await _unitRep.AsQueryable() .Where(u => u.TenantId == tenantId && u.FactoryId == factoryId && !u.IsDeleted && substepIds.Contains(u.SubstepId)) .OrderBy(u => u.SortNo) .ToListAsync(); var unitsBySubstepId = units .GroupBy(u => u.SubstepId) .ToDictionary(g => g.Key, g => g.ToList()); var exceptionCounts = await MapExceptionCountsAsync(tenantId, factoryId, new List { orderCode }); var listItem = MapOrderListItem(order, stages, exceptionCounts); var detail = BuildDetail(listItem, stages, substepsByFlow, unitsBySubstepId); return new AdoS8OrderFlowChainDto { Order = detail, ProcurementPivot = null, // t3c 切片接入 }; } // ─────────────── private mappers ─────────────── /// 从 ado_s8_exception 实时聚合每个 order_code 的异常计数 + 状态。 private async Task> MapExceptionCountsAsync( long tenantId, long factoryId, List orderCodes) { var result = orderCodes.ToDictionary(c => c, _ => (Count: 0, Status: string.Empty)); if (orderCodes.Count == 0) return result; var rows = await _exceptionRep.AsQueryable() .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted) .Where(e => e.SourceObjectType == ExceptionSourceObjectType) .Where(e => orderCodes.Contains(e.RelatedObjectCode!)) .Select(e => new { e.RelatedObjectCode, e.Severity }) .ToListAsync(); foreach (var grp in rows.Where(r => !string.IsNullOrEmpty(r.RelatedObjectCode)) .GroupBy(r => r.RelatedObjectCode!)) { var items = grp.ToList(); var status = items.Any(x => string.Equals(x.Severity, "SERIOUS", StringComparison.OrdinalIgnoreCase)) ? "SERIOUS" : (items.Count > 0 ? "FOLLOW" : string.Empty); result[grp.Key] = (items.Count, status); } return result; } private static AdoS8OrderFlowOrderListItemDto MapOrderListItem( AdoS8OrderFlowOrder o, List? lifecycle, Dictionary exceptionCounts) { var current = lifecycle?.FirstOrDefault(s => s.OrderFlowCode == o.CurrentOrderFlowCode); var counts = exceptionCounts.TryGetValue(o.OrderCode, out var c) ? c : (Count: 0, Status: string.Empty); return new AdoS8OrderFlowOrderListItemDto { OrderCode = o.OrderCode, ProductName = o.ProductName, ProductLine = o.ProductLine, CustomerCode = o.CustomerCode, CustomerName = o.CustomerName, CustomerType = o.CustomerType, Region = o.Region, Priority = o.Priority, WorkflowStatus = o.WorkflowStatus, CurrentOrderFlowCode = o.CurrentOrderFlowCode, CurrentOrderFlowName = OrderFlowConstants.ResolveName(o.CurrentOrderFlowCode), CurrentStatus = current?.Status ?? string.Empty, ReleaseAt = o.ReleaseAt, TargetCycleDays = o.TargetCycleDays, ActualCycleDays = o.ActualCycleDays, CurrentCycleDays = current?.ActualDays, NodeVarianceDays = current?.NodeVarianceDays, CumulativeVarianceDays = current?.CumulativeVarianceDays, ExceptionCount = counts.Count, ExceptionStatus = counts.Status, ResponseMinutes = o.ResponseMinutes, ProcessingMinutes = o.ProcessingMinutes, TotalLossMinutes = o.TotalLossMinutes, ScenarioCode = o.ScenarioCode, DataSource = o.DataSource, }; } private static AdoS8OrderFlowOrderDetailDto BuildDetail( AdoS8OrderFlowOrderListItemDto listItem, List stages, Dictionary>? substepsByFlow, Dictionary>? unitsBySubstepId) { var detail = new AdoS8OrderFlowOrderDetailDto { OrderCode = listItem.OrderCode, ProductName = listItem.ProductName, ProductLine = listItem.ProductLine, CustomerCode = listItem.CustomerCode, CustomerName = listItem.CustomerName, CustomerType = listItem.CustomerType, Region = listItem.Region, Priority = listItem.Priority, WorkflowStatus = listItem.WorkflowStatus, CurrentOrderFlowCode = listItem.CurrentOrderFlowCode, CurrentOrderFlowName = listItem.CurrentOrderFlowName, CurrentStatus = listItem.CurrentStatus, ReleaseAt = listItem.ReleaseAt, TargetCycleDays = listItem.TargetCycleDays, ActualCycleDays = listItem.ActualCycleDays, CurrentCycleDays = listItem.CurrentCycleDays, NodeVarianceDays = listItem.NodeVarianceDays, CumulativeVarianceDays = listItem.CumulativeVarianceDays, ExceptionCount = listItem.ExceptionCount, ExceptionStatus = listItem.ExceptionStatus, ResponseMinutes = listItem.ResponseMinutes, ProcessingMinutes = listItem.ProcessingMinutes, TotalLossMinutes = listItem.TotalLossMinutes, ScenarioCode = listItem.ScenarioCode, DataSource = listItem.DataSource, Lifecycle = stages.Select(s => MapStage(s, substepsByFlow, unitsBySubstepId)).ToList(), }; return detail; } private static AdoS8OrderFlowStageDto MapStage( AdoS8OrderFlowStage s, Dictionary>? substepsByFlow, Dictionary>? unitsBySubstepId) { var substeps = substepsByFlow != null && substepsByFlow.TryGetValue(s.OrderFlowCode, out var list) ? list.Select(x => MapSubstep(x, unitsBySubstepId)).ToList() : new List(); return new AdoS8OrderFlowStageDto { OrderFlowCode = s.OrderFlowCode, OrderFlowName = string.IsNullOrEmpty(s.OrderFlowName) ? OrderFlowConstants.ResolveName(s.OrderFlowCode) : s.OrderFlowName, OwnerDept = s.OwnerDept, SortNo = s.SortNo, PlannedDays = s.PlannedDays, ActualDays = s.ActualDays, TargetAt = s.TargetAt, ActualStartAt = s.ActualStartAt, ActualEndAt = s.ActualEndAt, Status = s.Status, NodeVarianceDays = s.NodeVarianceDays, CumulativeVarianceDays = s.CumulativeVarianceDays, Substeps = substeps, }; } private static AdoS8OrderFlowSubstepDto MapSubstep( AdoS8OrderFlowSubstep x, Dictionary>? unitsBySubstepId) { var units = unitsBySubstepId != null && unitsBySubstepId.TryGetValue(x.Id, out var list) ? list.Select(MapUnit).ToList() : new List(); return new AdoS8OrderFlowSubstepDto { SubstepCode = x.SubstepCode, SubstepName = x.SubstepName, PiHours = x.PiHours, ActualHours = x.ActualHours, Status = x.Status, SortNo = x.SortNo, Units = units, }; } private static AdoS8OrderFlowSubstepUnitDto MapUnit(AdoS8OrderFlowSubstepUnit u) => new() { UnitCode = u.UnitCode, UnitName = u.UnitName, PiHours = u.PiHours, ActualHours = u.ActualHours, Status = u.Status, SortNo = u.SortNo, }; // ────────────────────────────────────────────────────────────────────── // t3c:Aggregate + ProcurementPivot // ────────────────────────────────────────────────────────────────────── /// /// 链路聚合视图: /// scope=BASELINE_PPT → 只读 ado_s8_order_flow_snapshot.stage_snapshots_json /// scope=CURRENT_FILTERED → 从 order + stage 实时聚合 /// snapshot 缺失时返回空骨架(不做 fallback 硬编码)。 /// public async Task GetAggregateAsync(AdoS8OrderFlowAggregateQueryDto query) { var tenantId = query.TenantId ?? 1; var factoryId = query.FactoryId ?? 1; var scope = string.IsNullOrWhiteSpace(query.Scope) ? ScopeBaselinePpt : query.Scope; if (string.Equals(scope, ScopeBaselinePpt, StringComparison.OrdinalIgnoreCase)) return await BuildBaselineAggregateAsync(tenantId, factoryId, scope); if (string.Equals(scope, ScopeCurrentFiltered, StringComparison.OrdinalIgnoreCase)) return await BuildCurrentAggregateAsync(tenantId, factoryId, scope, query.OrderCodes); return EmptyAggregate(scope); } private async Task BuildBaselineAggregateAsync(long tenantId, long factoryId, string scope) { var snapshot = await _snapshotRep.AsQueryable() .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted) .Where(s => s.ScopeCode == ScopeBaselinePpt && s.SnapshotCode == BaselineSnapshotCode) .FirstAsync(); if (snapshot == null) return EmptyAggregate(scope); var stages = ParseStageSnapshots(snapshot.StageSnapshotsJson); var collab = await BuildFinalAssemblyCollabSummaryAsync(tenantId, factoryId, orderCodes: null); return new AdoS8OrderFlowAggregateDto { Scope = scope, TotalOrders = snapshot.TotalOrders, TotalCustomers = snapshot.TotalCustomers, AvgResponseMinutes = snapshot.AvgResponseMinutes, AvgProcessingMinutes = snapshot.AvgProcessingMinutes, AvgLossMinutes = snapshot.AvgLossMinutes, StageAggregates = ReorderByCanonical(stages), FinalAssemblyCollabSummary = collab, }; } private async Task BuildCurrentAggregateAsync( long tenantId, long factoryId, string scope, List? orderCodes) { var orders = await _orderRep.AsQueryable() .Where(o => o.TenantId == tenantId && o.FactoryId == factoryId && !o.IsDeleted) .WhereIF(orderCodes != null && orderCodes.Count > 0, o => orderCodes!.Contains(o.OrderCode)) .ToListAsync(); if (orders.Count == 0) return EmptyAggregate(scope); var orderIds = orders.Select(o => o.Id).ToList(); var stages = await _stageRep.AsQueryable() .Where(s => s.TenantId == tenantId && s.FactoryId == factoryId && !s.IsDeleted) .Where(s => orderIds.Contains(s.OrderId)) .ToListAsync(); var responseList = orders.Where(o => o.ResponseMinutes.HasValue).Select(o => (decimal)o.ResponseMinutes!.Value).ToList(); var processingList = orders.Where(o => o.ProcessingMinutes.HasValue).Select(o => (decimal)o.ProcessingMinutes!.Value).ToList(); var lossList = orders.Where(o => o.TotalLossMinutes.HasValue).Select(o => (decimal)o.TotalLossMinutes!.Value).ToList(); var stagesByFlow = stages.GroupBy(s => s.OrderFlowCode).ToDictionary(g => g.Key, g => g.ToList()); var stageAggregates = new List(); foreach (var code in OrderFlowConstants.All) { stagesByFlow.TryGetValue(code, out var rows); stageAggregates.Add(BuildStageAggregateFromRows(code, rows ?? new List())); } var collab = await BuildFinalAssemblyCollabSummaryAsync(tenantId, factoryId, orderCodes); return new AdoS8OrderFlowAggregateDto { Scope = scope, TotalOrders = orders.Count, TotalCustomers = orders.Select(o => o.CustomerCode).Where(c => !string.IsNullOrEmpty(c)).Distinct().Count(), AvgResponseMinutes = AvgOrZero(responseList), AvgProcessingMinutes = AvgOrZero(processingList), AvgLossMinutes = AvgOrZero(lossList), StageAggregates = stageAggregates, FinalAssemblyCollabSummary = collab, }; } // ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:FINAL_ASSEMBLY_DELIVERY 末端协同派生。 // 仅查 ado_s8_exception 现有数据;只覆盖有真实映射的 2 个主门禁 + 1 个并行准备,其它 6 个对象前端展示 "--"。 // 不读取 S0|S4 业务表;不新增表 / seed / 异常类型。 private async Task BuildFinalAssemblyCollabSummaryAsync( long tenantId, long factoryId, List? orderCodes) { var allTypes = GoodsHandoverExceptionTypes .Concat(ShipmentConfirmationExceptionTypes) .Concat(ShippingPlanExceptionTypes) .Distinct() .ToList(); var rows = await _exceptionRep.AsQueryable() .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted) .Where(e => e.SourceObjectType == ExceptionSourceObjectType) .Where(e => e.ExceptionTypeCode != null && allTypes.Contains(e.ExceptionTypeCode!)) .WhereIF(orderCodes != null && orderCodes.Count > 0, e => e.RelatedObjectCode != null && orderCodes!.Contains(e.RelatedObjectCode!)) .Select(e => new ExceptionAttribution { ExceptionTypeCode = e.ExceptionTypeCode, RelatedObjectCode = e.RelatedObjectCode, Title = e.Title, Severity = e.Severity, CreatedAt = e.CreatedAt, }) .ToListAsync(); return new AdoS8FinalAssemblyCollabSummaryDto { Gates = new Dictionary { [FinalAssemblyObjectGoodsHandover] = BuildObjectStat(rows, GoodsHandoverExceptionTypes, isGate: true), [FinalAssemblyObjectShipmentConfirmation] = BuildObjectStat(rows, ShipmentConfirmationExceptionTypes, isGate: true), }, Parallels = new Dictionary { [FinalAssemblyObjectShippingPlan] = BuildObjectStat(rows, ShippingPlanExceptionTypes, isGate: false), }, }; } private static AdoS8FinalAssemblyObjectStatDto BuildObjectStat( List rows, string[] candidateTypes, bool isGate) { var subset = rows.Where(r => r.ExceptionTypeCode != null && candidateTypes.Contains(r.ExceptionTypeCode!)).ToList(); var count = subset .Where(r => !string.IsNullOrEmpty(r.RelatedObjectCode)) .Select(r => r.RelatedObjectCode!) .Distinct() .Count(); var top = subset .OrderByDescending(r => string.Equals(r.Severity, "SERIOUS", StringComparison.OrdinalIgnoreCase) ? 1 : 0) .ThenByDescending(r => r.CreatedAt) .FirstOrDefault(); return new AdoS8FinalAssemblyObjectStatDto { ImpactedOrderCount = isGate ? count : (int?)null, RiskOrderCount = isGate ? (int?)null : count, TopRiskTitle = top?.Title, TopRiskSeverity = top?.Severity, }; } private sealed class ExceptionAttribution { public string? ExceptionTypeCode { get; set; } public string? RelatedObjectCode { get; set; } public string? Title { get; set; } public string? Severity { get; set; } public DateTime CreatedAt { get; set; } } private static AdoS8OrderFlowStageAggregateDto BuildStageAggregateFromRows(string flowCode, List rows) { var dto = new AdoS8OrderFlowStageAggregateDto { OrderFlowCode = flowCode, OrderFlowName = OrderFlowConstants.ResolveName(flowCode), }; if (rows.Count == 0) return dto; var plannedList = rows.Select(r => r.PlannedDays).ToList(); var actualList = rows.Where(r => r.ActualDays.HasValue && !string.Equals(r.Status, "pending", StringComparison.OrdinalIgnoreCase)) .Select(r => r.ActualDays!.Value).ToList(); var green = rows.Count(r => string.Equals(r.Status, "green", StringComparison.OrdinalIgnoreCase)); var yellow = rows.Count(r => string.Equals(r.Status, "yellow", StringComparison.OrdinalIgnoreCase)); var red = rows.Count(r => string.Equals(r.Status, "red", StringComparison.OrdinalIgnoreCase)); var pending = rows.Count(r => string.Equals(r.Status, "pending", StringComparison.OrdinalIgnoreCase)); var total = rows.Count; dto.KpiAvgDays = AvgOrZero(plannedList); dto.ActualAvgDays = AvgOrZero(actualList); dto.Green = green; dto.Yellow = yellow; dto.Red = red; dto.Pending = pending; // 整数百分比;total 不含 0 因为上面已 short-circuit。 percent 因子 100 是公式系数,非业务真值。 dto.OnTimeRate = total > 0 ? (int)Math.Round(green * 100.0 / total) : 0; return dto; } private static List ParseStageSnapshots(string? json) { if (string.IsNullOrWhiteSpace(json)) return new List(); try { var parsed = JsonSerializer.Deserialize>(json, JsonOpts); return parsed ?? new List(); } catch (JsonException) { return new List(); } } /// 按 OrderFlowConstants.All 重排;缺失 code 补空骨架;多余 code 追加在尾部以避免静默丢数据。 private static List ReorderByCanonical(List input) { var byCode = input.Where(x => !string.IsNullOrEmpty(x.OrderFlowCode)) .GroupBy(x => x.OrderFlowCode) .ToDictionary(g => g.Key, g => g.First()); var ordered = new List(); foreach (var code in OrderFlowConstants.All) { if (byCode.TryGetValue(code, out var hit)) { if (string.IsNullOrEmpty(hit.OrderFlowName)) hit.OrderFlowName = OrderFlowConstants.ResolveName(code); ordered.Add(hit); byCode.Remove(code); } else { ordered.Add(new AdoS8OrderFlowStageAggregateDto { OrderFlowCode = code, OrderFlowName = OrderFlowConstants.ResolveName(code), }); } } ordered.AddRange(byCode.Values); return ordered; } private static AdoS8OrderFlowAggregateDto EmptyAggregate(string scope) { var stages = OrderFlowConstants.All .Select(code => new AdoS8OrderFlowStageAggregateDto { OrderFlowCode = code, OrderFlowName = OrderFlowConstants.ResolveName(code), }) .ToList(); return new AdoS8OrderFlowAggregateDto { Scope = scope, StageAggregates = stages }; } private static decimal AvgOrZero(List values) => values.Count == 0 ? 0m : values.Average(); /// /// 采购透视视图: /// scope=BASELINE_PPT → 读 ado_s8_order_flow_procurement_pivot baseline 行(order_id=NULL) /// status 严格只读 DB;cycle_days 保持 decimal(6,3) 不截断。 /// 本切片正式只支持 BASELINE_PPT;其他 scope 返回空骨架并标注 scope。 /// public async Task GetProcurementPivotAsync(AdoS8OrderFlowProcurementPivotQueryDto query) { var tenantId = query.TenantId ?? 1; var factoryId = query.FactoryId ?? 1; var scope = string.IsNullOrWhiteSpace(query.Scope) ? ScopeBaselinePpt : query.Scope; if (!string.Equals(scope, ScopeBaselinePpt, StringComparison.OrdinalIgnoreCase)) return new AdoS8OrderFlowProcurementPivotDto { Scope = scope }; var rows = await _pivotRep.AsQueryable() .Where(p => p.TenantId == tenantId && p.FactoryId == factoryId && !p.IsDeleted) .Where(p => p.OrderId == null) // baseline 行 .ToListAsync(); return BuildPivot(scope, rows); } private static AdoS8OrderFlowProcurementPivotDto BuildPivot(string scope, List rows) { var result = new AdoS8OrderFlowProcurementPivotDto { Scope = scope }; if (rows.Count == 0) return result; var byMaterial = rows.GroupBy(r => r.MaterialCode) .ToDictionary(g => g.Key, g => g.ToList()); foreach (var (material, list) in byMaterial.OrderBy(kv => kv.Key, StringComparer.Ordinal)) { // KeyMaterials:取 (supplier=TOTAL, spec=TOTAL) 单元作为该 material 的概览 var grand = list.FirstOrDefault(r => r.SupplierCode == PivotTotalCode && r.SpecCode == PivotTotalCode); if (grand != null) { result.KeyMaterials.Add(new AdoS8OrderFlowKeyMaterialDto { MaterialCode = material, AvgCycleDays = grand.CycleDays, CycleStatus = grand.Status, ImpactCount = grand.ImpactCount, KitRate = grand.KitRate, ResultStatus = grand.Status, }); } // SupplierBreakdown:spec=TOTAL 的所有 supplier 行(含 supplier=TOTAL) foreach (var row in list.Where(r => r.SpecCode == PivotTotalCode) .OrderBy(r => r.SupplierCode == PivotTotalCode ? 1 : 0) .ThenBy(r => r.SupplierCode, StringComparer.Ordinal)) { result.SupplierBreakdown.Add(new AdoS8OrderFlowSupplierBreakdownDto { MaterialCode = material, SupplierCode = row.SupplierCode, AvgCycleDays = row.CycleDays, Status = row.Status, }); } // SpecBreakdown:supplier=TOTAL 的所有 spec 行(含 spec=TOTAL) foreach (var row in list.Where(r => r.SupplierCode == PivotTotalCode) .OrderBy(r => r.SpecCode == PivotTotalCode ? 1 : 0) .ThenBy(r => r.SpecCode, StringComparer.Ordinal)) { result.SpecBreakdown.Add(new AdoS8OrderFlowSpecBreakdownDto { MaterialCode = material, SpecCode = row.SpecCode, AvgCycleDays = row.CycleDays, Status = row.Status, }); } // MatrixByMaterial:每个 supplier 一行,cells keyed by spec_code(保留全部 cycle_days 精度) var matrix = list.GroupBy(r => r.SupplierCode) .OrderBy(g => g.Key == PivotTotalCode ? 1 : 0) .ThenBy(g => g.Key, StringComparer.Ordinal) .Select(g => new AdoS8OrderFlowProcurementMatrixRowDto { SupplierCode = g.Key, Cells = g.ToDictionary( r => r.SpecCode, r => new AdoS8OrderFlowProcurementMatrixCellDto { CycleDays = r.CycleDays, Status = r.Status, }), }) .ToList(); result.MatrixByMaterial[material] = matrix; } return result; } // ────────────────────────────────────────────────────────────────────── // S8-ORDER-CHAIN-PRODUCT-DESIGN-DRAWING-PERSIST-1:产品设计图号粒度聚合。 // 单一数据源 = ado_s8_order_flow_product_design_drawing 表; // 台数 / 占比 / 加权达成率 全部由 product_quantity 字段驱动; // 平均设计周期 = actual_days 算术平均(非 quantity 加权); // summary.categories 固定返回 STANDARD / NON_STANDARD / TOTAL 三档; // drawings 按 productType 过滤;未达标优先 + actualDays 降序 + orderCode/drawingNo 升序。 // ────────────────────────────────────────────────────────────────────── private const string ProductDesignTypeStandard = "STANDARD"; private const string ProductDesignTypeNonStandard = "NON_STANDARD"; private const string ProductDesignTypeTotal = "TOTAL"; private const decimal ProductDesignStageKpiDays = 3m; // S8-ORDER-CHAIN-PRODUCT-DESIGN-PPT-STATIC-AND-SINGLE-ORDER-ALIGN-1: // 非标产品 KPI 与常规产品同阶段标准 3 天;非标 actual_days 超 3 时记 red/未达标。 private const decimal ProductDesignNonStandardKpiDays = 3m; public async Task GetProductDesignDrawingsAsync( AdoS8OrderFlowProductDesignDrawingsQueryDto query) { var tenantId = query.TenantId ?? 1; var factoryId = query.FactoryId ?? 1; var orderCodes = ParseOrderCodesCsv(query.OrderCodes); var productTypeFilter = NormalizeProductDesignType(query.ProductType); var scope = orderCodes.Count == 0 ? ScopeBaselinePpt : ScopeCurrentFiltered; var rows = await _pddRep.AsQueryable() .Where(d => d.TenantId == tenantId && d.FactoryId == factoryId && !d.IsDeleted) .WhereIF(orderCodes.Count > 0, d => orderCodes.Contains(d.OrderCode)) .ToListAsync(); // summary 始终基于命中订单范围的全分类汇总(不受 productType 过滤影响),让前端可同时渲染三档汇总。 var summary = BuildProductDesignSummary(rows); var drawingItems = rows.AsEnumerable(); if (productTypeFilter != null) drawingItems = drawingItems.Where(d => d.ProductType == productTypeFilter); var orderedDrawings = drawingItems .OrderBy(d => d.IsAchieved) .ThenByDescending(d => d.ActualDays ?? decimal.MinValue) .ThenBy(d => d.OrderCode, StringComparer.Ordinal) .ThenBy(d => d.DrawingNo, StringComparer.Ordinal) .Select(MapProductDesignDrawingItem) .ToList(); return new AdoS8OrderFlowProductDesignDrawingsDto { Scope = scope, Filter = new AdoS8OrderFlowProductDesignFilterDto { OrderCodes = orderCodes, ProductType = productTypeFilter, }, Summary = summary, Drawings = orderedDrawings, }; } private static List ParseOrderCodesCsv(string? raw) { if (string.IsNullOrWhiteSpace(raw)) return new List(); return raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(s => !string.IsNullOrWhiteSpace(s)) .Distinct(StringComparer.Ordinal) .ToList(); } private static string? NormalizeProductDesignType(string? raw) { if (string.IsNullOrWhiteSpace(raw)) return null; var s = raw.Trim().ToUpperInvariant(); return s == ProductDesignTypeStandard || s == ProductDesignTypeNonStandard ? s : null; } private static AdoS8OrderFlowProductDesignSummaryDto BuildProductDesignSummary( List rows) { var stdRows = rows.Where(r => r.ProductType == ProductDesignTypeStandard).ToList(); var nonStdRows = rows.Where(r => r.ProductType == ProductDesignTypeNonStandard).ToList(); var grandTotalQty = rows.Sum(r => r.ProductQuantity); return new AdoS8OrderFlowProductDesignSummaryDto { Overall = BuildProductDesignOverall(rows), Categories = new List { BuildProductDesignCategory(ProductDesignTypeStandard, "常规产品", ProductDesignStageKpiDays, stdRows, grandTotalQty), BuildProductDesignCategory(ProductDesignTypeNonStandard, "非标产品", ProductDesignNonStandardKpiDays, nonStdRows, grandTotalQty), BuildProductDesignCategory(ProductDesignTypeTotal, "合计", ProductDesignStageKpiDays, rows, grandTotalQty), }, }; } private static AdoS8OrderFlowProductDesignOverallSummaryDto BuildProductDesignOverall( List rows) { var totalQty = rows.Sum(r => r.ProductQuantity); var achievedQty = rows.Where(r => r.IsAchieved).Sum(r => r.ProductQuantity); var actualDays = rows.Where(r => r.ActualDays.HasValue).Select(r => r.ActualDays!.Value).ToList(); return new AdoS8OrderFlowProductDesignOverallSummaryDto { DrawingCount = rows.Select(r => r.DrawingNo).Distinct(StringComparer.Ordinal).Count(), TotalQuantity = totalQty, KpiDays = ProductDesignStageKpiDays, AvgActualDays = actualDays.Count == 0 ? 0m : actualDays.Average(), AchievementRate = totalQty == 0 ? 0m : (decimal)achievedQty / totalQty, }; } private static AdoS8OrderFlowProductDesignCategorySummaryDto BuildProductDesignCategory( string productType, string name, decimal kpiDays, List rows, int grandTotalQty) { var totalQty = rows.Sum(r => r.ProductQuantity); var achievedQty = rows.Where(r => r.IsAchieved).Sum(r => r.ProductQuantity); var actualDays = rows.Where(r => r.ActualDays.HasValue).Select(r => r.ActualDays!.Value).ToList(); var rate = totalQty == 0 ? 0m : (decimal)achievedQty / totalQty; // S8-ORDER-CHAIN-PRODUCT-DESIGN-PPT-STATIC-AND-SINGLE-ORDER-ALIGN-1:分类汇总按业务口径差异化输出。 // STANDARD / TOTAL:KPI、平均设计周期、达成率均返回 null,由前端展示 --。 // NON_STANDARD:返回真实 KPI=3 天、actual_days 算术平均、quantity 加权达成率与对应状态色。 var isNonStandard = productType == ProductDesignTypeNonStandard; return new AdoS8OrderFlowProductDesignCategorySummaryDto { ProductType = productType, Name = name, DrawingCount = rows.Select(r => r.DrawingNo).Distinct(StringComparer.Ordinal).Count(), TotalQuantity = totalQty, Ratio = grandTotalQty == 0 ? 0m : (decimal)totalQty / grandTotalQty, KpiDays = isNonStandard ? kpiDays : (decimal?)null, AvgActualDays = isNonStandard ? (actualDays.Count == 0 ? (decimal?)null : actualDays.Average()) : (decimal?)null, AchievementRate = isNonStandard ? rate : (decimal?)null, Status = isNonStandard ? ClassifyProductDesignCategoryStatus(rate, totalQty) : string.Empty, }; } /// 达标率 ≥ 0.95 绿 / ≥ 0.80 黄 / 否则 红;无数据返回空串以表达未取数而非状态判定。 private static string ClassifyProductDesignCategoryStatus(decimal achievementRate, int totalQty) { if (totalQty == 0) return string.Empty; if (achievementRate >= 0.95m) return "green"; if (achievementRate >= 0.80m) return "yellow"; return "red"; } private static AdoS8OrderFlowProductDesignDrawingItemDto MapProductDesignDrawingItem( AdoS8OrderFlowProductDesignDrawing d) => new() { OrderCode = d.OrderCode, DrawingNo = d.DrawingNo, ProductType = d.ProductType, ResponsiblePerson = d.ResponsiblePerson, PlannedStartDate = d.PlannedStartDate, PlannedEndDate = d.PlannedEndDate, ActualStartDate = d.ActualStartDate, ActualEndDate = d.ActualEndDate, KpiDays = d.KpiDays, ActualDays = d.ActualDays, IsAchieved = d.IsAchieved, Status = d.Status, ProductQuantity = d.ProductQuantity, }; }