using Admin.NET.Plugin.AiDOP.Entity; using Admin.NET.Plugin.AiDOP.Infrastructure; namespace Admin.NET.Plugin.AiDOP.Controllers; public partial class AidopKanbanController { /// /// 智慧诊断:按模块和当前 L1 指标返回 L2/L3/L4 诊断矩阵。 /// [HttpGet("smart-diagnosis/{moduleCode}")] public async Task GetSmartDiagnosis( string moduleCode, [FromQuery] string? metricCode = null, [FromQuery] long factoryId = 1, [FromQuery] string? dateStart = null, [FromQuery] string? dateEnd = null, [FromQuery] string? product = null, [FromQuery] string? orderNo = null, [FromQuery] string? productionLine = null, [FromQuery] string? customer = null, [FromQuery] string? supplier = null, [FromQuery] string? material = null, [FromQuery] string? poNo = null, [FromQuery] string? warehouse = null, [FromQuery] string? workOrder = null, [FromQuery] string? equipment = null, [FromQuery] string? outboundNo = null) { var requested = (moduleCode ?? "").Trim().ToUpperInvariant(); if (requested == "S8") return await GetSmartDiagnosisFromS8ExceptionsAsync(factoryId, dateStart, dateEnd, orderNo); var mc = NormalizeDynamicDashboardModule(moduleCode); if (mc == null) return BadRequest(new { message = "moduleCode 仅支持 S1/S2/S3/S4/S5/S6/S7/S8/S9" }); var scope = await _trustedScope.ResolveAsync(factoryId); var tenantId = scope.TenantId; factoryId = scope.FactoryId; var filter = ParseDashboardFilter( dateStart, dateEnd, product, orderNo, productionLine, customer, supplier, material, poNo, warehouse, workOrder, equipment, outboundNo); var kpis = await _db.Queryable() .Where(x => x.TenantId == tenantId && x.ModuleCode == mc && x.IsEnabled) .OrderBy(x => x.MetricLevel) .OrderBy(x => x.SortNo) .ToListAsync(); var valueBundle = await ResolveDashboardValuesBundleAsync(tenantId, factoryId, mc, filter, kpis); var values = valueBundle.Values; var snaps = values.ToDictionary( x => x.Key, x => new SmartDiagnosisTree.ValueSnap(x.Value.MetricValue, x.Value.TargetValue), StringComparer.OrdinalIgnoreCase); var l1Metrics = kpis.Where(x => x.MetricLevel == 1).OrderBy(x => x.SortNo).ToList(); var resolved = SmartDiagnosisTree.ResolveRoot(l1Metrics, snaps, metricCode); if (resolved.Error != null) return BadRequest(new { message = resolved.Error }); var root = resolved.Root; if (root == null) return Ok(new { moduleCode = mc, metricCode, title = $"{mc} 智慧诊断", stages = Array.Empty(), hasChildren = false }); var children = SmartDiagnosisTree.DirectChildren(kpis, root.Id, 2); var stagePayload = children.Select((kpi, index) => { var current = BuildDiagnosisValue(kpi, values); var l3Rows = SmartDiagnosisTree.DirectChildren(kpis, kpi.Id, 3); var l4Rows = l3Rows.ToDictionary( x => x.MetricCode, x => SmartDiagnosisTree.DirectChildren(kpis, x.Id, 4) .Select(y => BuildDiagnosisNode(y, values)) .ToList(), StringComparer.OrdinalIgnoreCase); return new { key = kpi.MetricCode, name = kpi.MetricName, flowCaption = kpi.MetricName, dept = string.IsNullOrWhiteSpace(kpi.Department) ? "未配置" : kpi.Department, kpi = FormatDiagnosisValue(current.Target, kpi.Unit), actualDays = FormatDiagnosisValue(current.Current, kpi.Unit), actualRate = SmartDiagnosisTree.BuildDiagnosisRate(current.Current, current.Target, kpi.Direction), status = current.Status, callout = BuildDiagnosisCallout(kpi.MetricName, current.Status, current.GapLabel), calloutTone = current.Status, sortNo = kpi.SortNo > 0 ? kpi.SortNo : index + 1, tier1 = l3Rows.Select(x => BuildDiagnosisNode(x, values)).ToList(), tier2 = l4Rows }; }).ToList(); var rootValue = BuildDiagnosisValue(root, values); var worstStage = stagePayload .OrderByDescending(x => SmartDiagnosisTree.DiagnosisSeverity(x.status)) .ThenBy(x => x.sortNo) .FirstOrDefault(); var selectionLabel = resolved.SelectionMode == SmartDiagnosisTree.SelectionGlobalWorst ? $"全局自动选择:{root.MetricName}" : $"当前指标:{root.MetricName}"; var evidence = await _evidence.QueryAsync(tenantId, factoryId, mc, filter); return Ok(new { moduleCode = mc, metricCode = root.MetricCode, metricName = root.MetricName, title = $"{mc} {ResolveDashboardModuleName(mc)} · 智慧诊断", subtitle = selectionLabel, selectionMode = resolved.SelectionMode, hasChildren = children.Count > 0, dataSource = BuildDataSourcePayload(valueBundle), evidence, root = new { metricCode = root.MetricCode, metricName = root.MetricName, currentValue = rootValue.Current, targetValue = rootValue.Target, unit = root.Unit ?? "", status = rootValue.Status, gapLabel = rootValue.GapLabel, department = string.IsNullOrWhiteSpace(root.Department) ? "未配置" : root.Department }, pmSummary = new { node = worstStage?.name ?? root.MetricName, leadDelta = rootValue.GapLabel ?? "0" }, stages = stagePayload }); } private async Task GetSmartDiagnosisFromS8ExceptionsAsync( long factoryId, string? dateStart, string? dateEnd, string? orderNo) { var scope = await _trustedScope.ResolveAsync(factoryId); var filter = ParseDashboardFilter( dateStart, dateEnd, null, orderNo, null, null, null, null, null, null, null, null, null); var evidence = await _evidence.QueryAsync(scope.TenantId, scope.FactoryId, "S8", filter); var first = evidence.Items.FirstOrDefault(); return Ok(new { moduleCode = "S8", metricCode = first == null ? null : "S8_EXCEPTION", metricName = first?.Title, title = "S8 异常监控 · 智慧诊断", subtitle = first == null ? "无可用 S8 异常佐证" : $"依据已存在异常:{first.ObjectCode}", selectionMode = "s8_exception", hasChildren = false, evidence, root = first == null ? null : new { metricCode = "S8_EXCEPTION", metricName = first.Title, currentValue = first.Contribution, targetValue = (decimal?)null, unit = "", status = first.Status, gapLabel = first.ObjectCode, department = "异常监控" }, stages = Array.Empty() }); } private static object BuildDiagnosisNode(AdoSmartOpsKpiMaster kpi, Dictionary values) { var v = BuildDiagnosisValue(kpi, values); return new { key = kpi.MetricCode, name = kpi.MetricName, plan = FormatDiagnosisValue(v.Target, kpi.Unit), actual = FormatDiagnosisValue(v.Current, kpi.Unit), status = v.Status, dept = string.IsNullOrWhiteSpace(kpi.Department) ? "未配置" : kpi.Department }; } private static DiagnosisValue BuildDiagnosisValue(AdoSmartOpsKpiMaster kpi, Dictionary values) { values.TryGetValue(kpi.MetricCode, out var value); var status = AidopS4KpiMerge.AchievementLevel( value?.MetricValue, value?.TargetValue, kpi.Direction ?? "higher_is_better", kpi.YellowThreshold, kpi.RedThreshold); var gap = AidopS4KpiMerge.GapValue(value?.MetricValue, value?.TargetValue); return new DiagnosisValue(value?.MetricValue, value?.TargetValue, status, FormatGapLabelGeneric(gap, kpi.Unit)); } private static string FormatDiagnosisValue(decimal? value, string? unit) { if (value == null) return "-"; var rounded = decimal.Round(value.Value, 2); return $"{rounded:0.##}{unit ?? ""}"; } private static string BuildDiagnosisCallout(string? metricName, string status, string? gapLabel) { var name = string.IsNullOrWhiteSpace(metricName) ? "当前指标" : metricName; return status switch { "red" => $"{name} 未达标,期量差 {gapLabel ?? "-"},建议优先定位责任环节。", "yellow" => $"{name} 存在预警,期量差 {gapLabel ?? "-"},建议持续跟踪。", "green" => $"{name} 达标,可作为后续环节输入。", _ => $"{name} 暂无完整诊断数据。" }; } private sealed record DiagnosisValue(decimal? Current, decimal? Target, string Status, string? GapLabel); }