|
@@ -23,6 +23,8 @@ import java.util.Collections;
|
|
|
|
|
|
|
|
import static org.hamcrest.Matchers.hasSize;
|
|
import static org.hamcrest.Matchers.hasSize;
|
|
|
import static org.hamcrest.Matchers.nullValue;
|
|
import static org.hamcrest.Matchers.nullValue;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.mock;
|
|
|
import static org.mockito.Mockito.verifyNoInteractions;
|
|
import static org.mockito.Mockito.verifyNoInteractions;
|
|
@@ -76,12 +78,14 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
.andExpect(jsonPath("$.data.summary.handledCount").value(2))
|
|
.andExpect(jsonPath("$.data.summary.handledCount").value(2))
|
|
|
.andExpect(jsonPath("$.data.summary.inProgressCount").value(3))
|
|
.andExpect(jsonPath("$.data.summary.inProgressCount").value(3))
|
|
|
.andExpect(jsonPath("$.data.summary.cancelledCount").value(1))
|
|
.andExpect(jsonPath("$.data.summary.cancelledCount").value(1))
|
|
|
- .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.2500"))
|
|
|
|
|
|
|
+ // 处置率=状态 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.numerator").value(2))
|
|
|
- .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(8))
|
|
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(5))
|
|
|
.andExpect(jsonPath("$.data.summary.handlingEfficiency.state").value("AVAILABLE"))
|
|
.andExpect(jsonPath("$.data.summary.handlingEfficiency.state").value("AVAILABLE"))
|
|
|
.andExpect(jsonPath("$.data.summary.handlingEfficiency.basis")
|
|
.andExpect(jsonPath("$.data.summary.handlingEfficiency.basis")
|
|
|
- .value("CLOSED_WORK_ORDERS_OVER_TOTAL_EVENTS"))
|
|
|
|
|
|
|
+ .value("CLOSED_WORK_ORDERS_OVER_ELIGIBLE_WORK_ORDERS"))
|
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.value").value("1.67"))
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.value").value("1.67"))
|
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.unit").value("HOURS"))
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.unit").value("HOURS"))
|
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.sampleCount").value(2))
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.sampleCount").value(2))
|
|
@@ -122,9 +126,11 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
.andExpect(status().isOk())
|
|
.andExpect(status().isOk())
|
|
|
.andExpect(jsonPath("$.data.eventFamily").value("ALARM"))
|
|
.andExpect(jsonPath("$.data.eventFamily").value("ALARM"))
|
|
|
.andExpect(jsonPath("$.data.summary.totalEvents").value(2))
|
|
.andExpect(jsonPath("$.data.summary.totalEvents").value(2))
|
|
|
- .andExpect(jsonPath("$.data.summary.handledCount").value(1))
|
|
|
|
|
- .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.5000"))
|
|
|
|
|
- .andExpect(jsonPath("$.data.workOrderStateCounts[5].count").value(1))
|
|
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handledCount").value(2))
|
|
|
|
|
+ // 处置率是**工单范围**读数,不随事件 family 变化(决策票 06:分母不要求事件回联)。
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.4000"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(5))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[5].count").value(2))
|
|
|
.andExpect(jsonPath("$.data.workOrderStateCounts[3].count").value(1));
|
|
.andExpect(jsonPath("$.data.workOrderStateCounts[3].count").value(1));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -152,7 +158,8 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
.andExpect(jsonPath("$.data.summary.totalEvents").value(0))
|
|
.andExpect(jsonPath("$.data.summary.totalEvents").value(0))
|
|
|
.andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.0000"))
|
|
.andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.0000"))
|
|
|
.andExpect(jsonPath("$.data.summary.handlingEfficiency.state").value("EMPTY"))
|
|
.andExpect(jsonPath("$.data.summary.handlingEfficiency.state").value("EMPTY"))
|
|
|
- .andExpect(jsonPath("$.data.summary.handlingEfficiency.reason").value("NO_EVENTS"))
|
|
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.reason")
|
|
|
|
|
+ .value("NO_ELIGIBLE_WORK_ORDERS"))
|
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.value").value("0.00"))
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.value").value("0.00"))
|
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.sampleCount").value(0))
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.sampleCount").value(0))
|
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.state").value("EMPTY"))
|
|
.andExpect(jsonPath("$.data.summary.averageDuration.state").value("EMPTY"))
|
|
@@ -203,8 +210,76 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
@Test
|
|
|
- void workOrderStateFilterNarrowsEventsAndMapFilterButNotStateCounts() throws Exception {
|
|
|
|
|
|
|
+ void rateDenominatorIncludesDelayingStateSixAndExcludesCancelledSeven() throws Exception {
|
|
|
|
|
+ when(adapter.load()).thenReturn(fixtureWithDelayingAndCancelledOrders());
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
|
|
+ .param("startTime", START)
|
|
|
|
|
+ .param("endTime", END))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ // 工单:状态 5、6、7、7 → 分母含 6 → 2(状态 5、6);状态 7 两张排除。
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.numerator").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(2))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.5000"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[6].count").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[7].count").value(2));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void rateIsAvailableFromWorkOrdersEvenWhenNoEventsFallInTheWindow() throws Exception {
|
|
|
|
|
+ when(adapter.load()).thenReturn(fixtureWithDelayingAndCancelledOrders());
|
|
|
|
|
+
|
|
|
mockMvc.perform(get(ENDPOINT)
|
|
mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
|
|
+ // 事件在 08:10,工单在 09:00:窗口排除全部事件但含工单。
|
|
|
|
|
+ .param("startTime", "2026-09-02T08:30:00+08:00")
|
|
|
|
|
+ .param("endTime", END))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.totalEvents").value(0))
|
|
|
|
|
+ // 决策票 06:分母不要求事件关联 → 没有事件也照样算处置率。
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.state").value("AVAILABLE"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.numerator").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(2))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.5000"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void summaryNumeratorAndDenominatorMatchTheReportedStateDistribution() throws Exception {
|
|
|
|
|
+ mockMvc.perform(get(ENDPOINT)
|
|
|
|
|
+ .param("eventFamily", "WARNING")
|
|
|
|
|
+ .param("startTime", START)
|
|
|
|
|
+ .param("endTime", END))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ // 自洽性:分母=状态 0–6 计数之和,分子=状态 5 计数,审计可复算。
|
|
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[0].count").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[3].count").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[4].count").value(1))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.workOrderStateCounts[5].count").value(2))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.numerator").value(2))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(5));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void dictionaryMatchesTheRuntimeWorkOrderStatusEnumCodeForCode() {
|
|
|
|
|
+ // 决策票 06:对外字典与运行时 WorkOrderStatusEnum 逐码对齐(0–7)。
|
|
|
|
|
+ for (com.zksy.ops.enums.WorkOrderStatusEnum runtime
|
|
|
|
|
+ : com.zksy.ops.enums.WorkOrderStatusEnum.values()) {
|
|
|
|
|
+ com.zksy.cockpit.workorder.CockpitWorkOrderState published =
|
|
|
|
|
+ com.zksy.cockpit.workorder.CockpitWorkOrderStateDictionary.byCode(runtime.getCode());
|
|
|
|
|
+ assertEquals(runtime.getCode(), Integer.valueOf(published.getCode()));
|
|
|
|
|
+ assertEquals(runtime.getDesc(), published.getName());
|
|
|
|
|
+ }
|
|
|
|
|
+ assertEquals(8, com.zksy.cockpit.workorder.CockpitWorkOrderStateDictionary.states().size());
|
|
|
|
|
+ // 处置率口径单源:分子 5、分母 0–6、状态 7 排除。
|
|
|
|
|
+ assertTrue(com.zksy.cockpit.workorder.CockpitWorkOrderStateDictionary.isRateEligible(0));
|
|
|
|
|
+ assertTrue(com.zksy.cockpit.workorder.CockpitWorkOrderStateDictionary.isRateEligible(6));
|
|
|
|
|
+ assertFalse(com.zksy.cockpit.workorder.CockpitWorkOrderStateDictionary.isRateEligible(7));
|
|
|
|
|
+ assertEquals(5, com.zksy.cockpit.workorder.CockpitWorkOrderStateDictionary.CLOSED_RATE_CODE);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void workOrderStateFilterNarrowsEventsAndMapFilterButNotStateCounts() throws Exception { mockMvc.perform(get(ENDPOINT)
|
|
|
.param("eventFamily", "WARNING")
|
|
.param("eventFamily", "WARNING")
|
|
|
.param("workOrderState", "5")
|
|
.param("workOrderState", "5")
|
|
|
.param("startTime", START)
|
|
.param("startTime", START)
|
|
@@ -359,7 +434,38 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
link("EW-006", 5, "2026-09-02T11:10:00+08:00", "2026-09-02T12:30:00+08:00"),
|
|
link("EW-006", 5, "2026-09-02T11:10:00+08:00", "2026-09-02T12:30:00+08:00"),
|
|
|
link("EW-007", 7, "2026-09-02T11:35:00+08:00", null),
|
|
link("EW-007", 7, "2026-09-02T11:35:00+08:00", null),
|
|
|
link("EA-001", 5, "2026-09-02T10:20:00+08:00", "2026-09-02T14:20:00+08:00"),
|
|
link("EA-001", 5, "2026-09-02T10:20:00+08:00", "2026-09-02T14:20:00+08:00"),
|
|
|
- link("EA-002", 3, "2026-09-02T11:00:00+08:00", null)));
|
|
|
|
|
|
|
+ link("EA-002", 3, "2026-09-02T11:00:00+08:00", null)),
|
|
|
|
|
+ // 范围内全部工单(分母来源):状态 0/3/4/5/5/7,共 6 张,
|
|
|
|
|
+ // 状态 7 排除 → 分母 5、分子 2(状态 5)。typeCode 供行业范围判定。
|
|
|
|
|
+ Arrays.asList(
|
|
|
|
|
+ scopeOrder("WO-S0", 0, "GAS_SENSOR"),
|
|
|
|
|
+ scopeOrder("WO-S3", 3, "WATER_SUPPLY_SENSOR"),
|
|
|
|
|
+ scopeOrder("WO-S4", 4, "SEWAGE_ENV"),
|
|
|
|
|
+ scopeOrder("WO-S5A", 5, "GAS_SENSOR"),
|
|
|
|
|
+ scopeOrder("WO-S5B", 5, "WATER_SUPPLY_SENSOR"),
|
|
|
|
|
+ scopeOrder("WO-S7", 7, "GAS_SENSOR")));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static CockpitHandlingEfficiencySnapshot.SourceWorkOrder scopeOrder(
|
|
|
|
|
+ String orderNo, Integer stateCode, String typeCode) {
|
|
|
|
|
+ return new CockpitHandlingEfficiencySnapshot.SourceWorkOrder(
|
|
|
|
|
+ orderNo, stateCode, "2026-09-02T09:00:00+08:00", typeCode);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 只含状态 5/6/7 的工单,用于验证分母含 6、排除 7。 */
|
|
|
|
|
+ private static CockpitHandlingEfficiencySnapshot fixtureWithDelayingAndCancelledOrders() {
|
|
|
|
|
+ return new CockpitHandlingEfficiencySnapshot(
|
|
|
|
|
+ "2026-09-02T11:59:50+08:00",
|
|
|
|
|
+ types(),
|
|
|
|
|
+ Collections.singletonList(
|
|
|
|
|
+ warning("EW-001", 1, "2026-09-02T08:10:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001")),
|
|
|
|
|
+ Collections.emptyList(),
|
|
|
|
|
+ Collections.emptyList(),
|
|
|
|
|
+ Arrays.asList(
|
|
|
|
|
+ scopeOrder("WO-D5", 5, "GAS_SENSOR"),
|
|
|
|
|
+ scopeOrder("WO-D6", 6, "GAS_SENSOR"),
|
|
|
|
|
+ scopeOrder("WO-D7A", 7, "GAS_SENSOR"),
|
|
|
|
|
+ scopeOrder("WO-D7B", 7, "GAS_SENSOR")));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static CockpitHandlingEfficiencySnapshot fixtureWithLegacyStateCodes() {
|
|
private static CockpitHandlingEfficiencySnapshot fixtureWithLegacyStateCodes() {
|
|
@@ -372,7 +478,11 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
Collections.emptyList(),
|
|
Collections.emptyList(),
|
|
|
Arrays.asList(
|
|
Arrays.asList(
|
|
|
link("EW-001", 99, "2026-09-02T08:10:00+08:00", null),
|
|
link("EW-001", 99, "2026-09-02T08:10:00+08:00", null),
|
|
|
- link("EW-002", 7, "2026-09-02T09:10:00+08:00", null)));
|
|
|
|
|
|
|
+ link("EW-002", 7, "2026-09-02T09:10:00+08:00", null)),
|
|
|
|
|
+ // 旧码 99 与 7 都经字典收敛为 7(已取消)→ 分母 0、计数[7]=2。
|
|
|
|
|
+ Arrays.asList(
|
|
|
|
|
+ scopeOrder("WO-L1", 99, "GAS_SENSOR"),
|
|
|
|
|
+ scopeOrder("WO-L2", 7, "GAS_SENSOR")));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static CockpitHandlingEfficiencySnapshot fixtureWithMixedDurations() {
|
|
private static CockpitHandlingEfficiencySnapshot fixtureWithMixedDurations() {
|
|
@@ -389,7 +499,12 @@ class CockpitHandlingEfficiencyControllerTest {
|
|
|
link("EW-001", 5, "2026-09-02T08:00:00+08:00", "2026-09-02T10:00:00+08:00"),
|
|
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-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-003", 5, "2026-09-02T10:00:00+08:00", null),
|
|
|
- link("EW-004", 3, "2026-09-02T11:00:00+08:00", null)));
|
|
|
|
|
|
|
+ link("EW-004", 3, "2026-09-02T11:00:00+08:00", null)),
|
|
|
|
|
+ // 三张已结案工单 → handledCount 3、分母 3、处置率 1.0000。
|
|
|
|
|
+ Arrays.asList(
|
|
|
|
|
+ scopeOrder("WO-M1", 5, "GAS_SENSOR"),
|
|
|
|
|
+ scopeOrder("WO-M2", 5, "GAS_SENSOR"),
|
|
|
|
|
+ scopeOrder("WO-M3", 5, "GAS_SENSOR")));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static java.util.List<CockpitHandlingEfficiencySnapshot.SourceType> types() {
|
|
private static java.util.List<CockpitHandlingEfficiencySnapshot.SourceType> types() {
|