| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- using Admin.NET.Plugin.AiDOP.Entity;
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- namespace Admin.NET.Plugin.AiDOP.Controllers;
- public partial class AidopKanbanController
- {
- /// <summary>
- /// 智慧诊断:按模块和当前 L1 指标返回 L2/L3/L4 诊断矩阵。
- /// </summary>
- [HttpGet("smart-diagnosis/{moduleCode}")]
- public async Task<IActionResult> 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<AdoSmartOpsKpiMaster>()
- .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<object>(), 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<IActionResult> 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<object>()
- });
- }
- private static object BuildDiagnosisNode(AdoSmartOpsKpiMaster kpi, Dictionary<string, DashboardMetricValueModel> 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<string, DashboardMetricValueModel> 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);
- }
|