|
@@ -0,0 +1,182 @@
|
|
|
|
|
+using Admin.NET.Plugin.AiDOP.Entity.S8;
|
|
|
|
|
+using Admin.NET.Plugin.AiDOP.Service.S8;
|
|
|
|
|
+using Xunit;
|
|
|
|
|
+
|
|
|
|
|
+namespace Admin.NET.Test.S8;
|
|
|
|
|
+
|
|
|
|
|
+/// <summary>
|
|
|
|
|
+/// S8-CONFIG-AUTHORITY-FIX-1:结果 KPI 的「停用即不显示」门禁。
|
|
|
|
|
+///
|
|
|
|
|
+/// 修复前的缺陷:<c>GetResultKpiSummaryAsync</c> 只按 mechanism / is_result_kpi / 租户作用域取行,
|
|
|
|
|
+/// <b>完全不读 enabled</b>。运行库里 5 条结果 KPI 全部 enabled=0(remark "[RESERVED] S9 result KPI"),
|
|
|
|
|
+/// 却照常出现在监控总览——配置页面的停用开关对业务零效力。
|
|
|
|
|
+///
|
|
|
|
|
+/// 本文件锁定的契约与失败后果:
|
|
|
|
|
+/// A. enabled=true → 纳入 → 否则正常指标消失,属功能倒退
|
|
|
|
|
+/// B. enabled=false → 不纳入 → 否则"停用"仍然显示,用户认为配置坏了
|
|
|
|
|
+/// C. is_result_kpi=false → 不纳入 → 否则机制字典项(Rule 向导用)会污染结果 KPI 卡片
|
|
|
|
|
+/// D. 门禁必须在**覆盖解析之后** → 这是最容易写错的一条:
|
|
|
|
|
+/// 若把 Enabled 下推成 SQL WHERE,租户 override enabled=false 的行会先被滤掉,
|
|
|
|
|
+/// 分组里只剩 enabled=true 的全局行 → 结果回落成"显示",与停用意图完全相反。
|
|
|
|
|
+/// E. 不触碰 Rule 机制字典 → 门禁只作用于结果 KPI 路径
|
|
|
|
|
+///
|
|
|
|
|
+/// 不接 DB、不接 DI、不触发任何真实规则。
|
|
|
|
|
+/// </summary>
|
|
|
|
|
+public class S8ResultKpiEnabledGateTests
|
|
|
|
|
+{
|
|
|
|
|
+ private const long TenantId = 838257186181189L;
|
|
|
|
|
+ private const long FactoryId = 838257186320453L;
|
|
|
|
|
+
|
|
|
|
|
+ private static AdoS8MonitorMetric Metric(
|
|
|
|
|
+ string code, bool enabled, bool isResultKpi = true,
|
|
|
|
|
+ long tenantId = 0, long factoryId = 0, string mechanism = "RATIO", int sortNo = 0) => new()
|
|
|
|
|
+ {
|
|
|
|
|
+ MetricCode = code,
|
|
|
|
|
+ MetricName = code,
|
|
|
|
|
+ Mechanism = mechanism,
|
|
|
|
|
+ IsResultKpi = isResultKpi,
|
|
|
|
|
+ Enabled = enabled,
|
|
|
|
|
+ TenantId = tenantId,
|
|
|
|
|
+ FactoryId = factoryId,
|
|
|
|
|
+ SortNo = sortNo
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ private static List<string> CodesOf(IEnumerable<AdoS8MonitorMetric> rows) =>
|
|
|
|
|
+ rows.Select(x => x.MetricCode).ToList();
|
|
|
|
|
+
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+ // A/B/C. 基础门禁
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void T1_EnabledResultKpi_IsReturned()
|
|
|
|
|
+ {
|
|
|
|
|
+ var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
|
|
|
|
|
+ {
|
|
|
|
|
+ Metric("ORDER_DELIVERY_RATE", enabled: true)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Equal(new[] { "ORDER_DELIVERY_RATE" }, CodesOf(result));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void T2_DisabledResultKpi_IsNotReturned()
|
|
|
|
|
+ {
|
|
|
|
|
+ // 运行库现状:5 条结果 KPI 全部 enabled=0,修复前它们照常显示。
|
|
|
|
|
+ var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
|
|
|
|
|
+ {
|
|
|
|
|
+ Metric("IQC_PASS_RATE", enabled: false)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Empty(result);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void T3_NonResultKpi_IsNotReturned()
|
|
|
|
|
+ {
|
|
|
|
|
+ // is_result_kpi=false 的是 Rule 向导用的机制字典项(DATE / VALUE_RANGE),
|
|
|
|
|
+ // 即使 enabled=true 也不属于结果 KPI 卡片。
|
|
|
|
|
+ // 注:调用方 SQL 已按 IsResultKpi 过滤,这里再断言一次,防止将来有人把过滤挪进本方法时写反。
|
|
|
|
|
+ var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
|
|
|
|
|
+ {
|
|
|
|
|
+ Metric("ORDER_DUE_AT", enabled: true, isResultKpi: false, mechanism: "DATE")
|
|
|
|
|
+ }.Where(m => m.IsResultKpi));
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Empty(result);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+ // D. 覆盖解析与门禁的先后顺序 —— 本文件最关键的两条
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void T4_TenantOverrideDisable_HidesMetric_EvenWhenGlobalEnabled()
|
|
|
|
|
+ {
|
|
|
|
|
+ // 全局启用 + 租户停用 → 租户看不到。
|
|
|
|
|
+ // 若实现把 Enabled 下推成 SQL WHERE,租户行会先被滤掉、只剩全局行,
|
|
|
|
|
+ // 结果反而变成"可见"——本用例正是为了钉死这个反向缺陷。
|
|
|
|
|
+ var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
|
|
|
|
|
+ {
|
|
|
|
|
+ Metric("ORDER_DELIVERY_RATE", enabled: true), // 平台全局
|
|
|
|
|
+ Metric("ORDER_DELIVERY_RATE", enabled: false, tenantId: TenantId, factoryId: FactoryId) // 租户覆盖:停用
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Empty(result);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void T5_TenantOverrideEnable_ShowsMetric_EvenWhenGlobalDisabled()
|
|
|
|
|
+ {
|
|
|
|
|
+ // 反向:全局停用 + 租户启用 → 租户可见。
|
|
|
|
|
+ // 覆盖语义由 OrderByDescending(FactoryId).ThenByDescending(TenantId) 承载,本用例确认它对两个方向都成立。
|
|
|
|
|
+ var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
|
|
|
|
|
+ {
|
|
|
|
|
+ Metric("PO_DELIVERY_RATE", enabled: false),
|
|
|
|
|
+ Metric("PO_DELIVERY_RATE", enabled: true, tenantId: TenantId, factoryId: FactoryId)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Equal(new[] { "PO_DELIVERY_RATE" }, CodesOf(result));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void T5b_EachMetricCode_ResolvesToExactlyOneEffectiveRow()
|
|
|
|
|
+ {
|
|
|
|
|
+ // 覆盖解析必须"每个 code 只留一条",否则同一 KPI 会在卡片上重复出现。
|
|
|
|
|
+ var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
|
|
|
|
|
+ {
|
|
|
|
|
+ Metric("WO_COMPLETION_RATE", enabled: true),
|
|
|
|
|
+ Metric("WO_COMPLETION_RATE", enabled: true, tenantId: TenantId),
|
|
|
|
|
+ Metric("WO_COMPLETION_RATE", enabled: true, tenantId: TenantId, factoryId: FactoryId)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Single(result);
|
|
|
|
|
+ // 工厂行优先级最高。
|
|
|
|
|
+ Assert.Equal(FactoryId, result[0].FactoryId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+ // E. Rule 机制字典不受影响
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void T6_RuleMechanismDictionary_IsUntouchedByTheGate()
|
|
|
|
|
+ {
|
|
|
|
|
+ // 运行库中 ado_s8_monitor_metric 有 8 条 enabled=1 的机制字典项(DATE / VALUE_RANGE),
|
|
|
|
|
+ // 由 S8ConfigDraftService(Rule 向导)消费。本门禁只在结果 KPI 路径调用,
|
|
|
|
|
+ // 这里断言:即使把它们混进候选集,也只会因 IsResultKpi=false 被调用方排除,
|
|
|
|
|
+ // 而不会被本方法当作结果 KPI 返回。
|
|
|
|
|
+ var mixed = new[]
|
|
|
|
|
+ {
|
|
|
|
|
+ Metric("ORDER_DUE_AT", enabled: true, isResultKpi: false, mechanism: "DATE"),
|
|
|
|
|
+ Metric("INV_QTY", enabled: true, isResultKpi: false, mechanism: "VALUE_RANGE"),
|
|
|
|
|
+ Metric("ORDER_FLOW_STAGE_VARIANCE", enabled: true, isResultKpi: false, mechanism: "VALUE_RANGE"),
|
|
|
|
|
+ Metric("ORDER_DELIVERY_RATE", enabled: true)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 模拟调用方 SQL 的 mechanism + is_result_kpi 过滤。
|
|
|
|
|
+ var candidates = mixed.Where(m => m.Mechanism == "RATIO" && m.IsResultKpi);
|
|
|
|
|
+ var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(candidates);
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Equal(new[] { "ORDER_DELIVERY_RATE" }, CodesOf(result));
|
|
|
|
|
+ // 机制字典项全部保持 enabled=true,未被本批改动。
|
|
|
|
|
+ Assert.All(mixed.Where(m => !m.IsResultKpi), m => Assert.True(m.Enabled));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+ // 排序稳定性(卡片顺序不应因门禁而乱)
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void EnabledMetrics_KeepSortNoThenCodeOrdering()
|
|
|
|
|
+ {
|
|
|
|
|
+ var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
|
|
|
|
|
+ {
|
|
|
|
|
+ Metric("B_RATE", enabled: true, sortNo: 2),
|
|
|
|
|
+ Metric("A_RATE", enabled: true, sortNo: 2),
|
|
|
|
|
+ Metric("Z_RATE", enabled: true, sortNo: 1),
|
|
|
|
|
+ Metric("X_RATE", enabled: false, sortNo: 0) // 停用项不参与排序结果
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Assert.Equal(new[] { "Z_RATE", "A_RATE", "B_RATE" }, CodesOf(result));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|