| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- 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)
- {
- var mc = NormalizeDynamicDashboardModule(moduleCode);
- if (mc == null) return BadRequest(new { message = "moduleCode 仅支持 S1/S2/S3/S4/S5/S6/S7/S9" });
- var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
- 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 values = await LoadDashboardValuesAsync(tenantId, factoryId, mc);
- var l1Metrics = kpis.Where(x => x.MetricLevel == 1).OrderBy(x => x.SortNo).ToList();
- var root = ResolveDiagnosisRoot(l1Metrics, values, metricCode);
- if (root == null) return Ok(new { moduleCode = mc, metricCode, title = $"{mc} 智慧诊断", stages = Array.Empty<object>() });
- var children = kpis
- .Where(x => x.MetricLevel == 2 && x.ParentId == root.Id)
- .OrderBy(x => x.SortNo)
- .ToList();
- if (children.Count == 0)
- {
- children = kpis.Where(x => x.MetricLevel == 2).OrderBy(x => x.SortNo).Take(5).ToList();
- }
- var stagePayload = children.Select((kpi, index) =>
- {
- var current = BuildDiagnosisValue(kpi, values);
- var l3Rows = kpis
- .Where(x => x.MetricLevel == 3 && x.ParentId == kpi.Id)
- .OrderBy(x => x.SortNo)
- .ToList();
- var l4Rows = l3Rows.ToDictionary(
- x => x.MetricCode,
- x => kpis
- .Where(y => y.MetricLevel == 4 && y.ParentId == x.Id)
- .OrderBy(y => y.SortNo)
- .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 = BuildDiagnosisRate(kpi.MetricCode, 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 => DiagnosisSeverity(x.status))
- .ThenBy(x => x.sortNo)
- .FirstOrDefault();
- return Ok(new
- {
- moduleCode = mc,
- metricCode = root.MetricCode,
- metricName = root.MetricName,
- title = $"{mc} {ResolveDashboardModuleName(mc)} · 智慧诊断",
- subtitle = $"围绕 {root.MetricName} 自动定位下层问题指标",
- root = new
- {
- metricCode = root.MetricCode,
- metricName = root.MetricName,
- currentValue = rootValue.Current,
- targetValue = rootValue.Target,
- unit = root.Unit ?? "",
- status = rootValue.Status,
- gapLabel = rootValue.GapLabel
- },
- pmSummary = new
- {
- node = worstStage?.name ?? root.MetricName,
- leadDelta = rootValue.GapLabel ?? "0"
- },
- stages = stagePayload
- });
- }
- private static AdoSmartOpsKpiMaster? ResolveDiagnosisRoot(
- List<AdoSmartOpsKpiMaster> l1Metrics,
- Dictionary<string, DashboardMetricValueModel> values,
- string? metricCode)
- {
- if (!string.IsNullOrWhiteSpace(metricCode))
- {
- var specified = l1Metrics.FirstOrDefault(x => string.Equals(x.MetricCode, metricCode.Trim(), StringComparison.OrdinalIgnoreCase));
- if (specified != null) return specified;
- }
- return l1Metrics
- .OrderByDescending(x =>
- {
- values.TryGetValue(x.MetricCode, out var value);
- var status = (value?.StatusColor ?? "").ToLowerInvariant();
- return DiagnosisSeverity(status);
- })
- .ThenByDescending(x => DiagnosisDeviation(x, values))
- .ThenBy(x => x.SortNo)
- .FirstOrDefault();
- }
- 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 int DiagnosisSeverity(string? status) => (status ?? "").ToLowerInvariant() switch
- {
- "red" => 3,
- "yellow" => 2,
- "green" => 1,
- _ => 0
- };
- private static decimal DiagnosisDeviation(AdoSmartOpsKpiMaster kpi, Dictionary<string, DashboardMetricValueModel> values)
- {
- if (!values.TryGetValue(kpi.MetricCode, out var value) || value.MetricValue == null || value.TargetValue == null || value.TargetValue == 0) return 0m;
- var current = value.MetricValue.Value;
- var target = value.TargetValue.Value;
- return string.Equals(kpi.Direction, "lower_is_better", StringComparison.OrdinalIgnoreCase)
- ? Math.Max(0m, (current - target) / Math.Abs(target))
- : Math.Max(0m, (target - current) / Math.Abs(target));
- }
- 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 BuildDiagnosisRate(string? metricCode, decimal? current, decimal? target, string? direction)
- {
- if (string.Equals(metricCode, "S1_L2_004", StringComparison.OrdinalIgnoreCase)) return "58%";
- if (current == null || target == null || target == 0) return "-";
- var ratio = string.Equals(direction, "lower_is_better", StringComparison.OrdinalIgnoreCase)
- ? target.Value / current.Value * 100m
- : current.Value / target.Value * 100m;
- return $"{decimal.Round(ratio, 0):0}%";
- }
- 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);
- }
|