|
|
@@ -1,5 +1,6 @@
|
|
|
package com.zksy.web.controller.cockpit;
|
|
|
|
|
|
+import com.zksy.base.warning.domain.WarningWorkflow;
|
|
|
import com.zksy.cockpit.event.CockpitHandlingEfficiencyAdapter;
|
|
|
import com.zksy.cockpit.event.CockpitHandlingEfficiencyService;
|
|
|
import com.zksy.cockpit.event.CockpitHandlingEfficiencySnapshot;
|
|
|
@@ -34,6 +35,17 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
|
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
|
|
|
|
+/**
|
|
|
+ * 预警效率端点契约(票 74 两族语义分流)。
|
|
|
+ *
|
|
|
+ * <p>两族各有一套口径,**不适用的一方显式为 null**:
|
|
|
+ * <ul>
|
|
|
+ * <li>报警族:工单八态 + 工单口径处置率 + 关联工单结案时长(工单是报警的处置手段)。</li>
|
|
|
+ * <li>预警族:预警自身六态 + 已解除率 + 发布→已解除平均时长;工单口径一律 null
|
|
|
+ * (预警不挂工单,工单读数对预警不成立 —— 票 65 实测 linkedWorkOrderCount=0)。</li>
|
|
|
+ * </ul>
|
|
|
+ * 预警族/报警族的断言因此各自成组:工单口径用例走 ALARM,预警流程用例走 WARNING。
|
|
|
+ */
|
|
|
class CockpitHandlingEfficiencyControllerTest {
|
|
|
|
|
|
private static final String ENDPOINT = "/api/cockpit/v1/events/handling-efficiency";
|
|
|
@@ -56,7 +68,7 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void warningFamilyExposesSummaryExplicitBasisEightStatesTrendAndEvents() throws Exception {
|
|
|
+ void warningFamilyExposesOwnWorkflowAndNullsWorkOrderCaliber() throws Exception {
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
.param("eventFamily", "WARNING")
|
|
|
.param("startTime", START)
|
|
|
@@ -72,53 +84,105 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
.andExpect(jsonPath("$.data.eventFamily").value("WARNING"))
|
|
|
.andExpect(jsonPath("$.data.filters.eventFamily").value("WARNING"))
|
|
|
.andExpect(jsonPath("$.data.filters.workOrderState").value(nullValue()))
|
|
|
+ // 事件口径两族共用(真实计数,不是第二轮读数)。
|
|
|
.andExpect(jsonPath("$.data.summary.totalEvents").value(8))
|
|
|
.andExpect(jsonPath("$.data.summary.linkedWorkOrderCount").value(6))
|
|
|
.andExpect(jsonPath("$.data.summary.missingWorkOrderCount").value(2))
|
|
|
- .andExpect(jsonPath("$.data.summary.handledCount").value(2))
|
|
|
- .andExpect(jsonPath("$.data.summary.inProgressCount").value(3))
|
|
|
- .andExpect(jsonPath("$.data.summary.cancelledCount").value(1))
|
|
|
- // 处置率=状态 5 ÷ 状态 0–6 工单数(决策票 06)。范围工单为 0,3,4,5,5,7:
|
|
|
- // 分子=2(状态 5)、分母=5(状态 0/3/4/5/5;状态 7 排除)→ 0.4000。
|
|
|
- .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.4000"))
|
|
|
- .andExpect(jsonPath("$.data.summary.handlingEfficiency.numerator").value(2))
|
|
|
- .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(5))
|
|
|
- .andExpect(jsonPath("$.data.summary.handlingEfficiency.state").value("AVAILABLE"))
|
|
|
- .andExpect(jsonPath("$.data.summary.handlingEfficiency.basis")
|
|
|
- .value("CLOSED_WORK_ORDERS_OVER_ELIGIBLE_WORK_ORDERS"))
|
|
|
- .andExpect(jsonPath("$.data.summary.averageDuration.value").value("1.67"))
|
|
|
- .andExpect(jsonPath("$.data.summary.averageDuration.unit").value("HOURS"))
|
|
|
- .andExpect(jsonPath("$.data.summary.averageDuration.sampleCount").value(2))
|
|
|
- .andExpect(jsonPath("$.data.summary.averageDuration.state").value("AVAILABLE"))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts", hasSize(8)))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[0].stateCode").value(0))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[0].stateName").value("待派单"))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[0].count").value(1))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[1].stateName").value("待接单"))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[1].count").value(0))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[2].stateName").value("已接单"))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[2].count").value(0))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[3].stateName").value("处理中"))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[3].count").value(1))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[4].stateName").value("待验收"))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[4].count").value(1))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[5].stateName").value("已结案"))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[5].count").value(2))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[6].stateName").value("延期中"))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[6].count").value(0))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[7].stateName").value("已取消"))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[7].count").value(1))
|
|
|
+ // 票 74:**工单口径**对预警族不成立 → 显式 null(不是 0,也不是别的族的数)。
|
|
|
+ .andExpect(jsonPath("$.data.summary.handledCount").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.summary.inProgressCount").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.summary.cancelledCount").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts").value(nullValue()))
|
|
|
+ // 预警自身六态全铺(缺的态计 0),顺序即 WarningWorkflow 主线。
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts", hasSize(6)))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[0].stateCode").value("DRAFT"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[0].stateName").value("草稿"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[0].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[1].stateCode").value("PENDING"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[1].stateName").value("待接收"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[1].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[2].stateCode").value("RECEIVED"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[2].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[3].stateCode").value("PROCESSING"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[3].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[4].stateCode").value("HANDLED"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[4].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[5].stateCode").value("CLOSED"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[5].stateName").value("已解除"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[5].count").value(3))
|
|
|
+ // 已解除率=已解除 ÷ 窗口内预警数(3/8),基线与工单口径不同名。
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency.numerator").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency.denominator").value(8))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency.value").value("0.3750"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency.state").value("AVAILABLE"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency.basis")
|
|
|
+ .value("CLOSED_WARNINGS_OVER_WINDOW_WARNINGS"))
|
|
|
+ // 发布→已解除平均时长:0.50h / 2.00h / 1.00h → 1.17h。
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningDuration.value").value("1.17"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningDuration.unit").value("HOURS"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningDuration.sampleCount").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningDuration.state").value("AVAILABLE"))
|
|
|
.andExpect(jsonPath("$.data.events", hasSize(8)))
|
|
|
.andExpect(jsonPath("$.data.events[0].eventNo").value("EW-007"))
|
|
|
.andExpect(jsonPath("$.data.events[0].hasWorkOrder").value(true))
|
|
|
.andExpect(jsonPath("$.data.pagination.total").value(8))
|
|
|
+ // 趋势桶:事件发布量照实给,工单结案量与结案时长两族口径 → null。
|
|
|
.andExpect(jsonPath("$.data.durationTrend", hasSize(5)))
|
|
|
.andExpect(jsonPath("$.data.durationTrend[0].timeBucket").value("2026-09-02T08:00:00+08:00"))
|
|
|
- .andExpect(jsonPath("$.data.durationTrend[4].timeBucket").value("2026-09-02T12:00:00+08:00"));
|
|
|
+ .andExpect(jsonPath("$.data.durationTrend[0].eventCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.durationTrend[0].handledCount").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.durationTrend[0].averageDuration").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.durationTrend[4].timeBucket").value("2026-09-02T12:00:00+08:00"))
|
|
|
+ .andExpect(jsonPath("$.data.durationTrend[4].eventCount").value(0));
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void alarmFamilyIsIndependentFromWarnings() throws Exception {
|
|
|
+ void warningFamilyPutsUnrecognisedStatusIntoUnknownBucketSoBucketsStillSumToTotal() throws Exception {
|
|
|
+ when(adapter.load()).thenReturn(fixtureWithDirtyWarningStatus());
|
|
|
+
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", START)
|
|
|
+ .param("endTime", END))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.summary.totalEvents").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts", hasSize(7)))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[3].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[5].count").value(1))
|
|
|
+ // 脏状态进 UNKNOWN(最后一个桶),不静默丢 —— 六个已知态 2 + UNKNOWN 1 = 3。
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[6].stateCode").value("UNKNOWN"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[6].stateName").value("未知"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[6].count").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency.numerator").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency.denominator").value(3));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void warningFamilyEmptyWindowReportsTypedEmptyStatesInsteadOfFakeZero() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
+ .param("startTime", "2026-09-05T00:00:00+08:00")
|
|
|
+ .param("endTime", "2026-09-05T02:00:00+08:00"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data.summary.totalEvents").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts", hasSize(6)))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[0].count").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts[5].count").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency.denominator").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency.reason").value("NO_WARNINGS"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningDuration.sampleCount").value(0))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningDuration.state").value("EMPTY"))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningDuration.reason").value("NO_CLOSED_WARNINGS"))
|
|
|
+ // 预警族照样没有工单口径。
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts").value(nullValue()));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void alarmFamilyKeepsWorkOrderCaliberAndNullsTheWarningSection() throws Exception {
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
.param("eventFamily", "ALARM")
|
|
|
.param("startTime", START)
|
|
|
@@ -127,17 +191,28 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
.andExpect(jsonPath("$.data.eventFamily").value("ALARM"))
|
|
|
.andExpect(jsonPath("$.data.summary.totalEvents").value(2))
|
|
|
.andExpect(jsonPath("$.data.summary.handledCount").value(2))
|
|
|
+ .andExpect(jsonPath("$.data.summary.inProgressCount").value(3))
|
|
|
+ .andExpect(jsonPath("$.data.summary.cancelledCount").value(1))
|
|
|
// 处置率是**工单范围**读数,不随事件 family 变化(决策票 06:分母不要求事件回联)。
|
|
|
.andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.4000"))
|
|
|
.andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(5))
|
|
|
+ // 报警族可算结案时长的样本只有 EA-001(EA-002 的工单还在处理中)→ 10:20→14:20 共 4 小时。
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.sampleCount").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.summary.averageDuration.value").value("4.00"))
|
|
|
.andExpect(jsonPath("$.data.workOrderStateCounts[5].count").value(2))
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[3].count").value(1));
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[3].count").value(1))
|
|
|
+ // 票 74:报警族不吃预警口径 —— 预警段显式 null,不用 0 冒充。
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningHandlingEfficiency").value(nullValue()))
|
|
|
+ .andExpect(jsonPath("$.data.summary.warningDuration").value(nullValue()))
|
|
|
+ // 报警族的趋势桶仍带工单口径(不是 null)。
|
|
|
+ .andExpect(jsonPath("$.data.durationTrend[0].handledCount").value(0));
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
void everyWorkOrderStateIsReportedUnmerged() throws Exception {
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
- .param("eventFamily", "WARNING")
|
|
|
+ .param("eventFamily", "ALARM")
|
|
|
.param("startTime", START)
|
|
|
.param("endTime", END))
|
|
|
.andExpect(status().isOk())
|
|
|
@@ -151,7 +226,7 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
@Test
|
|
|
void emptyWindowReportsEmptyEfficiencyAndDurationStates() throws Exception {
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
- .param("eventFamily", "WARNING")
|
|
|
+ .param("eventFamily", "ALARM")
|
|
|
.param("startTime", "2026-09-05T00:00:00+08:00")
|
|
|
.param("endTime", "2026-09-05T02:00:00+08:00"))
|
|
|
.andExpect(status().isOk())
|
|
|
@@ -164,11 +239,12 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.sampleCount").value(0))
|
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.state").value("EMPTY"))
|
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.reason")
|
|
|
- .value("NO_CLOSED_WORK_ORDERS"));
|
|
|
+ .value("NO_CLOSED_WORK_ORDERS"))
|
|
|
+ .andExpect(jsonPath("$.data.warningStateCounts").value(nullValue()));
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void eventsWithoutWorkOrdersAreCountedAsMissingAndKeepStateCountsStable() throws Exception {
|
|
|
+ void eventsWithoutWorkOrdersAreCountedAsMissing() throws Exception {
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
.param("eventFamily", "WARNING")
|
|
|
.param("startTime", START)
|
|
|
@@ -184,7 +260,7 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
when(adapter.load()).thenReturn(fixtureWithLegacyStateCodes());
|
|
|
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
- .param("eventFamily", "WARNING")
|
|
|
+ .param("eventFamily", "ALARM")
|
|
|
.param("startTime", START)
|
|
|
.param("endTime", END))
|
|
|
.andExpect(status().isOk())
|
|
|
@@ -200,7 +276,7 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
when(adapter.load()).thenReturn(fixtureWithMixedDurations());
|
|
|
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
- .param("eventFamily", "WARNING")
|
|
|
+ .param("eventFamily", "ALARM")
|
|
|
.param("startTime", START)
|
|
|
.param("endTime", END))
|
|
|
.andExpect(status().isOk())
|
|
|
@@ -214,7 +290,7 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
when(adapter.load()).thenReturn(fixtureWithDelayingAndCancelledOrders());
|
|
|
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
- .param("eventFamily", "WARNING")
|
|
|
+ .param("eventFamily", "ALARM")
|
|
|
.param("startTime", START)
|
|
|
.param("endTime", END))
|
|
|
.andExpect(status().isOk())
|
|
|
@@ -231,8 +307,8 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
when(adapter.load()).thenReturn(fixtureWithDelayingAndCancelledOrders());
|
|
|
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
- .param("eventFamily", "WARNING")
|
|
|
- // 事件在 08:10,工单在 09:00:窗口排除全部事件但含工单。
|
|
|
+ .param("eventFamily", "ALARM")
|
|
|
+ // 工单在 09:00,窗口 08:30 起:窗口排除事件但含工单。
|
|
|
.param("startTime", "2026-09-02T08:30:00+08:00")
|
|
|
.param("endTime", END))
|
|
|
.andExpect(status().isOk())
|
|
|
@@ -247,7 +323,7 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
@Test
|
|
|
void summaryNumeratorAndDenominatorMatchTheReportedStateDistribution() throws Exception {
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
- .param("eventFamily", "WARNING")
|
|
|
+ .param("eventFamily", "ALARM")
|
|
|
.param("startTime", START)
|
|
|
.param("endTime", END))
|
|
|
.andExpect(status().isOk())
|
|
|
@@ -279,17 +355,19 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void workOrderStateFilterNarrowsEventsAndMapFilterButNotStateCounts() throws Exception { mockMvc.perform(get(ENDPOINT)
|
|
|
- .param("eventFamily", "WARNING")
|
|
|
+ void workOrderStateFilterNarrowsEventsAndMapFilterButNotStateCounts() throws Exception {
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
+ .param("eventFamily", "ALARM")
|
|
|
.param("workOrderState", "5")
|
|
|
.param("startTime", START)
|
|
|
.param("endTime", END))
|
|
|
.andExpect(status().isOk())
|
|
|
.andExpect(jsonPath("$.data.filters.workOrderState").value(5))
|
|
|
- .andExpect(jsonPath("$.data.pagination.total").value(2))
|
|
|
- .andExpect(jsonPath("$.data.events", hasSize(2)))
|
|
|
+ .andExpect(jsonPath("$.data.pagination.total").value(1))
|
|
|
+ .andExpect(jsonPath("$.data.events", hasSize(1)))
|
|
|
.andExpect(jsonPath("$.data.mapFilter.workOrderState").value(5))
|
|
|
- .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(2)))
|
|
|
+ .andExpect(jsonPath("$.data.mapFilter.deviceCodes", hasSize(1)))
|
|
|
+ // 状态分布是**范围工单**口径,不随 workOrderState 过滤收窄。
|
|
|
.andExpect(jsonPath("$.data.workOrderStateCounts[5].count").value(2))
|
|
|
.andExpect(jsonPath("$.data.workOrderStateCounts[7].count").value(1));
|
|
|
}
|
|
|
@@ -409,19 +487,32 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
&& !method.isAnnotationPresent(DeleteMapping.class);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 主样本:8 条在范围内预警(`EW-LEGACY` 的类型不在设备类型表里,被行业范围判定排除),
|
|
|
+ * 状态覆盖六态 —— DRAFT 1 / PENDING 1 / RECEIVED 1 / PROCESSING 1 / HANDLED 1 / CLOSED 3,
|
|
|
+ * 其中三条 CLOSED 的 `update_time` 分别距发布 0.50h / 2.00h / 1.00h。
|
|
|
+ */
|
|
|
private static CockpitHandlingEfficiencySnapshot fixture() {
|
|
|
return new CockpitHandlingEfficiencySnapshot(
|
|
|
"2026-09-02T11:59:50+08:00",
|
|
|
types(),
|
|
|
Arrays.asList(
|
|
|
- warning("EW-001", 1, "2026-09-02T08:10:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001"),
|
|
|
- warning("EW-002", 2, "2026-09-02T08:40:00+08:00", "GAS_SENSOR", "D-GAS-2", "燃气浓度计-002"),
|
|
|
- warning("EW-003", 2, "2026-09-02T09:20:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-1", "供水压力计-001"),
|
|
|
- warning("EW-004", 3, "2026-09-02T10:05:00+08:00", "SEWAGE_ENV", "D-DR-1", "污水环境监测仪-001"),
|
|
|
- warning("EW-005", 4, "2026-09-02T10:45:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-2", "供水压力计-002"),
|
|
|
- warning("EW-006", 1, "2026-09-02T11:10:00+08:00", "GAS_SENSOR", "D-GAS-3", "燃气浓度计-003"),
|
|
|
- warning("EW-007", 2, "2026-09-02T11:35:00+08:00", "SEWAGE_ENV", "D-DR-2", "污水环境监测仪-002"),
|
|
|
- warning("EW-008", 1, "2026-09-02T09:55:00+08:00", "SEWAGE_ENV", "D-DR-3", "污水环境监测仪-003"),
|
|
|
+ warningWithWorkflow("EW-001", 1, "2026-09-02T08:10:00+08:00", "GAS_SENSOR",
|
|
|
+ "D-GAS-1", "燃气浓度计-001", WarningWorkflow.CLOSED, "2026-09-02T08:40:00+08:00"),
|
|
|
+ warningWithWorkflow("EW-002", 2, "2026-09-02T08:40:00+08:00", "GAS_SENSOR",
|
|
|
+ "D-GAS-2", "燃气浓度计-002", WarningWorkflow.PROCESSING, null),
|
|
|
+ warningWithWorkflow("EW-003", 2, "2026-09-02T09:20:00+08:00", "WATER_SUPPLY_SENSOR",
|
|
|
+ "D-WS-1", "供水压力计-001", WarningWorkflow.RECEIVED, null),
|
|
|
+ warningWithWorkflow("EW-004", 3, "2026-09-02T10:05:00+08:00", "SEWAGE_ENV",
|
|
|
+ "D-DR-1", "污水环境监测仪-001", WarningWorkflow.CLOSED, "2026-09-02T12:05:00+08:00"),
|
|
|
+ warningWithWorkflow("EW-005", 4, "2026-09-02T10:45:00+08:00", "WATER_SUPPLY_SENSOR",
|
|
|
+ "D-WS-2", "供水压力计-002", WarningWorkflow.PENDING, null),
|
|
|
+ warningWithWorkflow("EW-006", 1, "2026-09-02T11:10:00+08:00", "GAS_SENSOR",
|
|
|
+ "D-GAS-3", "燃气浓度计-003", WarningWorkflow.HANDLED, null),
|
|
|
+ warningWithWorkflow("EW-007", 2, "2026-09-02T11:35:00+08:00", "SEWAGE_ENV",
|
|
|
+ "D-DR-2", "污水环境监测仪-002", WarningWorkflow.CLOSED, "2026-09-02T12:35:00+08:00"),
|
|
|
+ warningWithWorkflow("EW-008", 1, "2026-09-02T09:55:00+08:00", "SEWAGE_ENV",
|
|
|
+ "D-DR-3", "污水环境监测仪-003", WarningWorkflow.DRAFT, null),
|
|
|
warning("EW-LEGACY", 1, "2026-09-02T09:05:00+08:00", "LEGACY", "D-LEGACY", "旧设备")),
|
|
|
Arrays.asList(
|
|
|
alarm("EA-001", 1, "2026-09-02T10:20:00+08:00", "GAS_SENSOR", "D-GAS-9", "燃气浓度计-009"),
|
|
|
@@ -446,6 +537,23 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
scopeOrder("WO-S7", 7, "GAS_SENSOR")));
|
|
|
}
|
|
|
|
|
|
+ /** 一条脏状态(非六态字典值):必须进 UNKNOWN 桶,六态 + UNKNOWN 之和仍等于窗口内条数。 */
|
|
|
+ private static CockpitHandlingEfficiencySnapshot fixtureWithDirtyWarningStatus() {
|
|
|
+ return new CockpitHandlingEfficiencySnapshot(
|
|
|
+ "2026-09-02T11:59:50+08:00",
|
|
|
+ types(),
|
|
|
+ Arrays.asList(
|
|
|
+ warningWithWorkflow("EW-U1", 1, "2026-09-02T08:10:00+08:00", "GAS_SENSOR",
|
|
|
+ "D-GAS-1", "燃气浓度计-001", WarningWorkflow.CLOSED, "2026-09-02T09:10:00+08:00"),
|
|
|
+ warningWithWorkflow("EW-U2", 1, "2026-09-02T09:10:00+08:00", "GAS_SENSOR",
|
|
|
+ "D-GAS-2", "燃气浓度计-002", WarningWorkflow.PROCESSING, null),
|
|
|
+ warningWithWorkflow("EW-U3", 1, "2026-09-02T10:10:00+08:00", "GAS_SENSOR",
|
|
|
+ "D-GAS-3", "燃气浓度计-003", "WEIRD", null)),
|
|
|
+ Collections.emptyList(),
|
|
|
+ Collections.emptyList(),
|
|
|
+ Collections.emptyList());
|
|
|
+ }
|
|
|
+
|
|
|
private static CockpitHandlingEfficiencySnapshot.SourceWorkOrder scopeOrder(
|
|
|
String orderNo, Integer stateCode, String typeCode) {
|
|
|
return new CockpitHandlingEfficiencySnapshot.SourceWorkOrder(
|
|
|
@@ -472,13 +580,13 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
return new CockpitHandlingEfficiencySnapshot(
|
|
|
"2026-09-02T11:59:50+08:00",
|
|
|
types(),
|
|
|
- Arrays.asList(
|
|
|
- warning("EW-001", 1, "2026-09-02T08:10:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001"),
|
|
|
- warning("EW-002", 1, "2026-09-02T09:10:00+08:00", "GAS_SENSOR", "D-GAS-2", "燃气浓度计-002")),
|
|
|
Collections.emptyList(),
|
|
|
Arrays.asList(
|
|
|
- link("EW-001", 99, "2026-09-02T08:10:00+08:00", null),
|
|
|
- link("EW-002", 7, "2026-09-02T09:10:00+08:00", null)),
|
|
|
+ alarm("EA-001", 1, "2026-09-02T08:10:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001"),
|
|
|
+ alarm("EA-002", 1, "2026-09-02T09:10:00+08:00", "GAS_SENSOR", "D-GAS-2", "燃气浓度计-002")),
|
|
|
+ Arrays.asList(
|
|
|
+ link("EA-001", 99, "2026-09-02T08:10:00+08:00", null),
|
|
|
+ link("EA-002", 7, "2026-09-02T09:10:00+08:00", null)),
|
|
|
// 旧码 99 与 7 都经字典收敛为 7(已取消)→ 分母 0、计数[7]=2。
|
|
|
Arrays.asList(
|
|
|
scopeOrder("WO-L1", 99, "GAS_SENSOR"),
|
|
|
@@ -489,17 +597,17 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
return new CockpitHandlingEfficiencySnapshot(
|
|
|
"2026-09-02T11:59:50+08:00",
|
|
|
types(),
|
|
|
- Arrays.asList(
|
|
|
- warning("EW-001", 1, "2026-09-02T08:00:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001"),
|
|
|
- warning("EW-002", 1, "2026-09-02T09:00:00+08:00", "GAS_SENSOR", "D-GAS-2", "燃气浓度计-002"),
|
|
|
- warning("EW-003", 1, "2026-09-02T10:00:00+08:00", "GAS_SENSOR", "D-GAS-3", "燃气浓度计-003"),
|
|
|
- warning("EW-004", 1, "2026-09-02T11:00:00+08:00", "GAS_SENSOR", "D-GAS-4", "燃气浓度计-004")),
|
|
|
Collections.emptyList(),
|
|
|
Arrays.asList(
|
|
|
- link("EW-001", 5, "2026-09-02T08:00:00+08:00", "2026-09-02T10:00:00+08:00"),
|
|
|
- link("EW-002", 5, "2026-09-02T09:00:00+08:00", "2026-09-02T11:00:00+08:00"),
|
|
|
- link("EW-003", 5, "2026-09-02T10:00:00+08:00", null),
|
|
|
- link("EW-004", 3, "2026-09-02T11:00:00+08:00", null)),
|
|
|
+ alarm("EA-001", 1, "2026-09-02T08:00:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001"),
|
|
|
+ alarm("EA-002", 1, "2026-09-02T09:00:00+08:00", "GAS_SENSOR", "D-GAS-2", "燃气浓度计-002"),
|
|
|
+ alarm("EA-003", 1, "2026-09-02T10:00:00+08:00", "GAS_SENSOR", "D-GAS-3", "燃气浓度计-003"),
|
|
|
+ alarm("EA-004", 1, "2026-09-02T11:00:00+08:00", "GAS_SENSOR", "D-GAS-4", "燃气浓度计-004")),
|
|
|
+ Arrays.asList(
|
|
|
+ link("EA-001", 5, "2026-09-02T08:00:00+08:00", "2026-09-02T10:00:00+08:00"),
|
|
|
+ link("EA-002", 5, "2026-09-02T09:00:00+08:00", "2026-09-02T11:00:00+08:00"),
|
|
|
+ link("EA-003", 5, "2026-09-02T10:00:00+08:00", null),
|
|
|
+ link("EA-004", 3, "2026-09-02T11:00:00+08:00", null)),
|
|
|
// 三张已结案工单 → handledCount 3、分母 3、处置率 1.0000。
|
|
|
Arrays.asList(
|
|
|
scopeOrder("WO-M1", 5, "GAS_SENSOR"),
|
|
|
@@ -523,6 +631,13 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
no, level, eventTime, typeCode, deviceCode, deviceName);
|
|
|
}
|
|
|
|
|
|
+ private static CockpitHandlingEfficiencySnapshot.SourceWarning warningWithWorkflow(
|
|
|
+ String no, Integer level, String eventTime, String typeCode, String deviceCode, String deviceName,
|
|
|
+ String status, String updateTime) {
|
|
|
+ return new CockpitHandlingEfficiencySnapshot.SourceWarning(
|
|
|
+ no, level, eventTime, typeCode, deviceCode, deviceName, status, updateTime);
|
|
|
+ }
|
|
|
+
|
|
|
private static CockpitHandlingEfficiencySnapshot.SourceAlarm alarm(
|
|
|
String id, Integer level, String eventTime, String typeCode, String deviceCode, String deviceName) {
|
|
|
return new CockpitHandlingEfficiencySnapshot.SourceAlarm(
|