Explorar el Código

fix(cockpit): 工单处置率改为状态 0-6 工单分母口径

按决策票 06 / 规格 10 修订处置率口径,废止旧事件分母语义:

- 分母由「事件总数」改为「状态 0-6 工单数之和」(状态 7 已取消排除),
  范围=当前筛选内全部工单,按 work_order.create_time 界定时间窗口,
  不要求关联报警/事件;分子仍为状态 5(已结案)。
- basis 改为 CLOSED_WORK_ORDERS_OVER_ELIGIBLE_WORK_ORDERS;
  分母为 0 时回 state=EMPTY + reason=NO_ELIGIBLE_WORK_ORDERS。
- 同口径一并修订三处:events/handling-efficiency、
  events/history-analysis(仅汇总,小时桶仍为事件桶)、
  overview/warning-settings(两 family 共用一份全范围工单读数)。
- workOrderStateCounts/workOrderStates 同步改为工单口径,
  使 sum(状态 0-6) 与分母自洽、可审计复算。
- 三个 snapshot 新增 scopeWorkOrders(含状态、create_time、typeCode),
  由适配器经 work_order.device_id 解析设备类型以支持行业收敛。
- 口径单源收敛到 CockpitWorkOrderStateDictionary
  (HANDLING_RATE_BASIS / CLOSED_RATE_CODE / CANCELLED_RATE_CODE /
  isRateEligible),三个服务不再各自硬编码状态常量。
- 契约测试补齐 0-7 字典逐码对齐、分母含状态 6、状态 7 排除、
  分母 0 不适用、分子 5、以及分母与事件解耦等覆盖。
- docs/cockpit/README.md 同步新口径说明。

验证:mvn -o -pl pipe-network-service/zksy-admin -am compile 通过;
-Dtest=Cockpit*Test 299 例 0 失败。

基线卡片:驾驶舱真实数据接线地图 handoff/baselines/work-order-rate.md
Kazerin hace 1 semana
padre
commit
3de679c896
Se han modificado 14 ficheros con 663 adiciones y 71 borrados
  1. 1 1
      docs/cockpit/README.md
  2. 126 11
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitHandlingEfficiencyControllerTest.java
  3. 25 3
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitHistoryAnalysisControllerTest.java
  4. 19 8
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitOverviewWarningSettingsControllerTest.java
  5. 55 5
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitOverviewWarningSettingsServiceTest.java
  6. 63 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/BusinessCockpitEventAdapter.java
  7. 63 18
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHandlingEfficiencyService.java
  8. 38 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHandlingEfficiencySnapshot.java
  9. 69 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHistoryAnalysisService.java
  10. 45 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHistoryAnalysisSnapshot.java
  11. 20 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/overview/BusinessCockpitOverviewWarningSettingsAdapter.java
  12. 86 19
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/overview/CockpitOverviewWarningSettingsService.java
  13. 29 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/overview/CockpitOverviewWarningSettingsSnapshot.java
  14. 24 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderStateDictionary.java

+ 1 - 1
docs/cockpit/README.md

@@ -22,7 +22,7 @@
 | 新鲜度 | `CURRENT` `STALE` `EXPIRED` `EMPTY` `FAILED` | 每次响应携带 `freshness` |
 | 错误类别 | `UNAUTHORIZED` `SERVER_ERROR` `INVALID_REQUEST` `SNAPSHOT_EXPIRED` `NOT_FOUND` | 401/500/400/400/404 |
 
-效率口径固定为 `basis = CLOSED_WORK_ORDERS_OVER_TOTAL_EVENTS`(已结案工单数 ÷ 事件总数);系统不判断预警真伪,契约中不存在任何准确率、误报率字段。
+效率口径固定为 `basis = CLOSED_WORK_ORDERS_OVER_ELIGIBLE_WORK_ORDERS`:分子=状态 5(已结案)工单数,分母=状态 0–6 工单数之和(状态 7 已取消排除),工单范围=当前筛选内**全部工单**(按 `work_order.create_time` 界定时间窗口,不要求关联报警/事件);分母为 0 时回 `state=EMPTY` + `reason=NO_ELIGIBLE_WORK_ORDERS`,前端显示「不适用」而非 0%。同一个工单范围口径同时用于 `events/handling-efficiency`、`events/history-analysis`(汇总)与 `overview/warning-settings`(两个 family 共用一份范围读数);历史分析的小时桶与预警设置的日趋势仍是**事件**时间桶,与汇总处置率口径不同。旧 `CLOSED_WORK_ORDERS_OVER_TOTAL_EVENTS`(已结案 ÷ 事件总数)与「状态 6 / 事件总数」口径已废止。系统不判断预警真伪,契约中不存在任何准确率、误报率字段。
 
 ## 3. 端点清单
 

+ 126 - 11
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitHandlingEfficiencyControllerTest.java

@@ -23,6 +23,8 @@ import java.util.Collections;
 
 import static org.hamcrest.Matchers.hasSize;
 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.mockito.Mockito.mock;
 import static org.mockito.Mockito.verifyNoInteractions;
@@ -76,12 +78,14 @@ class CockpitHandlingEfficiencyControllerTest {
                 .andExpect(jsonPath("$.data.summary.handledCount").value(2))
                 .andExpect(jsonPath("$.data.summary.inProgressCount").value(3))
                 .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.denominator").value(8))
+                .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_TOTAL_EVENTS"))
+                        .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))
@@ -122,9 +126,11 @@ class CockpitHandlingEfficiencyControllerTest {
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.data.eventFamily").value("ALARM"))
                 .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));
     }
 
@@ -152,7 +158,8 @@ class CockpitHandlingEfficiencyControllerTest {
                 .andExpect(jsonPath("$.data.summary.totalEvents").value(0))
                 .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.0000"))
                 .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.sampleCount").value(0))
                 .andExpect(jsonPath("$.data.summary.averageDuration.state").value("EMPTY"))
@@ -203,8 +210,76 @@ class CockpitHandlingEfficiencyControllerTest {
     }
 
     @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)
+                        .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("workOrderState", "5")
                         .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-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-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() {
@@ -372,7 +478,11 @@ class CockpitHandlingEfficiencyControllerTest {
                 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)));
+                        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() {
@@ -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-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)));
+                        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() {

+ 25 - 3
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitHistoryAnalysisControllerTest.java

@@ -84,6 +84,8 @@ class CockpitHistoryAnalysisControllerTest {
                 .andExpect(jsonPath("$.data.summary.eventCount").value(5))
                 .andExpect(jsonPath("$.data.summary.handledCount").value(2))
                 .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.rows", hasSize(4)))
                 .andExpect(jsonPath("$.data.rows[0].timeBucket").value(BUCKET_23))
@@ -116,8 +118,10 @@ class CockpitHistoryAnalysisControllerTest {
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.data.eventFamily").value("ALARM"))
                 .andExpect(jsonPath("$.data.summary.eventCount").value(2))
+                // handledCount 仍是事件口径(行级同维);处置率是工单范围读数。
                 .andExpect(jsonPath("$.data.summary.handledCount").value(1))
-                .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.5000"))
+                .andExpect(jsonPath("$.data.summary.handlingEfficiency.value").value("0.4000"))
+                .andExpect(jsonPath("$.data.summary.handlingEfficiency.numerator").value(2))
                 .andExpect(jsonPath("$.data.rows[2].eventCount").value(2))
                 .andExpect(jsonPath("$.data.rows[2].handledCount").value(1))
                 .andExpect(jsonPath("$.data.rows[0].eventCount").value(0))
@@ -142,7 +146,10 @@ class CockpitHistoryAnalysisControllerTest {
         CockpitHistoryAnalysisResponse response = service("WARNING", START, END);
         assertEquals(5, response.getSummary().getEventCount());
         assertEquals(2, response.getSummary().getHandledCount());
+        // 汇总处置率=状态 5 ÷ 状态 0–6 工单数(决策票 06)→ 2/5。
         assertEquals("0.4000", response.getSummary().getHandlingEfficiency().getValue().toPlainString());
+        assertEquals(2, response.getSummary().getHandlingEfficiency().getNumerator());
+        assertEquals(5, response.getSummary().getHandlingEfficiency().getDenominator());
     }
 
     @Test
@@ -154,7 +161,8 @@ class CockpitHistoryAnalysisControllerTest {
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.data.summary.eventCount").value(0))
                 .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.rows", hasSize(3)))
                 .andExpect(jsonPath("$.data.rows[0].eventCount").value(0))
                 .andExpect(jsonPath("$.data.rows[2].handlingEfficiency.state").value("EMPTY"));
@@ -318,7 +326,21 @@ class CockpitHistoryAnalysisControllerTest {
                         workOrder("HW-003", 1),
                         workOrder("HW-004", 5),
                         workOrder("HW-005", 7),
-                        workOrder("HA-001", 5)));
+                        workOrder("HA-001", 5)),
+                // 汇总处置率的工单范围(分母来源):状态 5/5/4/1/0 → 分母 5、分子 2(状态 5)
+                // → 0.4000;状态 7 与关联关系无关,故不受 workOrders 影响。
+                Arrays.asList(
+                        scopeOrder(5, "GAS_SENSOR"),
+                        scopeOrder(5, "WATER_SUPPLY_SENSOR"),
+                        scopeOrder(4, "SEWAGE_ENV"),
+                        scopeOrder(1, "GAS_SENSOR"),
+                        scopeOrder(0, "GAS_SENSOR")));
+    }
+
+    private static CockpitHistoryAnalysisSnapshot.SourceWorkOrder scopeOrder(
+            Integer stateCode, String typeCode) {
+        return new CockpitHistoryAnalysisSnapshot.SourceWorkOrder(
+                stateCode, "2026-09-02T01:00:00+08:00", typeCode);
     }
 
     private static CockpitHistoryAnalysisSnapshot.SourceType type(String code, String industry) {

+ 19 - 8
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitOverviewWarningSettingsControllerTest.java

@@ -73,14 +73,17 @@ class CockpitOverviewWarningSettingsControllerTest {
                 .andExpect(jsonPath("$.data.warning.total").value(3))
                 .andExpect(jsonPath("$.data.warning.handled").value(1))
                 .andExpect(jsonPath("$.data.warning.pending").value(2))
-                .andExpect(jsonPath("$.data.warning.handlingEfficiency.value").value("0.3333"))
+                // 处置率改为工单口径(决策票 06):分子=状态 5(1),分母=状态 0–6(5)→ 0.2000。
+                .andExpect(jsonPath("$.data.warning.handlingEfficiency.value").value("0.2000"))
                 .andExpect(jsonPath("$.data.warning.handlingEfficiency.state").value("AVAILABLE"))
                 .andExpect(jsonPath("$.data.warning.handlingEfficiency.numerator").value(1))
-                .andExpect(jsonPath("$.data.warning.handlingEfficiency.denominator").value(3))
+                .andExpect(jsonPath("$.data.warning.handlingEfficiency.denominator").value(5))
                 .andExpect(jsonPath("$.data.warning.trend[0].handlingEfficiency.value").value("0.5000"))
                 .andExpect(jsonPath("$.data.warning.trend[0].handlingEfficiency.numerator").value(1))
                 .andExpect(jsonPath("$.data.warning.trend[0].handlingEfficiency.denominator").value(2))
+                // 状态分布同为工单口径(逐态补 0)。
                 .andExpect(jsonPath("$.data.warning.workOrderStates['1']").value(1))
+                .andExpect(jsonPath("$.data.warning.workOrderStates['4']").value(2))
                 .andExpect(jsonPath("$.data.warning.workOrderStates['5']").value(1))
                 .andExpect(jsonPath("$.data.warning.workOrderStates['6']").value(1))
                 .andExpect(jsonPath("$.data.warning.unlinkedEventCount").value(0))
@@ -88,9 +91,10 @@ class CockpitOverviewWarningSettingsControllerTest {
                 .andExpect(jsonPath("$.data.alarm.handled").value(0))
                 .andExpect(jsonPath("$.data.alarm.pending").value(2))
                 .andExpect(jsonPath("$.data.alarm.cancelledCount").value(0))
-                .andExpect(jsonPath("$.data.alarm.handlingEfficiency.value").value("0.0000"))
-                .andExpect(jsonPath("$.data.alarm.handlingEfficiency.numerator").value(0))
-                .andExpect(jsonPath("$.data.alarm.handlingEfficiency.denominator").value(2))
+                // 两 family 共用同一份全范围工单读数。
+                .andExpect(jsonPath("$.data.alarm.handlingEfficiency.value").value("0.2000"))
+                .andExpect(jsonPath("$.data.alarm.handlingEfficiency.numerator").value(1))
+                .andExpect(jsonPath("$.data.alarm.handlingEfficiency.denominator").value(5))
                 .andExpect(jsonPath("$.data.mapFilter.eventFamily").value("WARNING"))
                 .andExpect(jsonPath("$.data.mapFilter.label").value("预警"));
     }
@@ -106,8 +110,7 @@ class CockpitOverviewWarningSettingsControllerTest {
                 .andExpect(jsonPath("$.data.selectedMode").value("ALARM"))
                 .andExpect(jsonPath("$.data.warning.total").value(3))
                 .andExpect(jsonPath("$.data.alarm.total").value(2))
-                .andExpect(jsonPath("$.data.alarm.workOrderStates['4']").value(2))
-                .andExpect(jsonPath("$.data.mapFilter.eventFamily").value("ALARM"))
+                .andExpect(jsonPath("$.data.alarm.workOrderStates['4']").value(2))                .andExpect(jsonPath("$.data.mapFilter.eventFamily").value("ALARM"))
                 .andExpect(jsonPath("$.data.mapFilter.label").value("报警"));
     }
 
@@ -274,7 +277,15 @@ class CockpitOverviewWarningSettingsControllerTest {
                         new CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint(
                                 "ALARM", "2026-08-20", 2, 1),
                         new CockpitOverviewWarningSettingsSnapshot.SourceTrendPoint(
-                                "ALARM", "2026-08-01", 1, 1)));
+                                "ALARM", "2026-08-01", 1, 1)),
+                // 全范围工单(分母来源):状态 1/5/6/4/4 → 分母 5(排除状态 7),分子 1(状态 5)。
+                Arrays.asList(
+                        scopeOrder(1), scopeOrder(5), scopeOrder(6), scopeOrder(4), scopeOrder(4)));
+    }
+
+    private static CockpitOverviewWarningSettingsSnapshot.SourceWorkOrder scopeOrder(Integer state) {
+        return new CockpitOverviewWarningSettingsSnapshot.SourceWorkOrder(
+                state, "2026-09-01T10:00:00+08:00");
     }
 }
 

+ 55 - 5
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitOverviewWarningSettingsServiceTest.java

@@ -38,15 +38,14 @@ class CockpitOverviewWarningSettingsServiceTest {
         CockpitOverviewWarningSettingsResponse response =
                 service.load("request-eight", "WARNING", null, null, null);
 
+        // 事件计数仍按 family 各自统计。
         assertEquals(Integer.valueOf(10), response.getWarning().getTotal());
         assertEquals(Integer.valueOf(1), response.getWarning().getHandled());
         assertEquals(Integer.valueOf(6), response.getWarning().getPending());
         assertEquals(Integer.valueOf(1), response.getWarning().getCancelledCount());
-        assertEquals("0.1000", response.getWarning().getHandlingEfficiency().getValue().toPlainString());
-        assertEquals(Integer.valueOf(1), response.getWarning().getHandlingEfficiency().getNumerator());
-        assertEquals(Integer.valueOf(10), response.getWarning().getHandlingEfficiency().getDenominator());
         assertEquals(Integer.valueOf(1), response.getWarning().getUnlinkedEventCount());
         assertEquals(Integer.valueOf(1), response.getWarning().getInvalidWorkOrderStateCount());
+        // 工单状态分布与处置率改为工单口径(决策票 06):逐态 1 张,共 8 张。
         assertEquals(Integer.valueOf(1), response.getWarning().getWorkOrderStates().get(0));
         assertEquals(Integer.valueOf(1), response.getWarning().getWorkOrderStates().get(1));
         assertEquals(Integer.valueOf(1), response.getWarning().getWorkOrderStates().get(2));
@@ -58,6 +57,39 @@ class CockpitOverviewWarningSettingsServiceTest {
         assertEquals(8, response.getWarning().getWorkOrderStates().size());
         assertTrue(response.getWarning().getWorkOrderStates().keySet().stream()
                 .allMatch(code -> code >= 0 && code <= 7));
+        // 分母=状态 0–6(7 张,状态 7 排除),分子=状态 5(1 张)→ 1/7。
+        assertEquals(Integer.valueOf(1), response.getWarning().getHandlingEfficiency().getNumerator());
+        assertEquals(Integer.valueOf(7), response.getWarning().getHandlingEfficiency().getDenominator());
+        assertEquals("0.1429", response.getWarning().getHandlingEfficiency().getValue().toPlainString());
+    }
+
+    @Test
+    void bothFamiliesShareTheSameWorkOrderScopedRate() {
+        when(adapter.load()).thenReturn(eightStateFixture());
+
+        CockpitOverviewWarningSettingsResponse response =
+                service.load("request-shared", "WARNING", null, null, null);
+
+        // 分母不要求事件回联 → 两 family 共用同一份全范围工单读数。
+        assertEquals(response.getWarning().getHandlingEfficiency().getDenominator(),
+                response.getAlarm().getHandlingEfficiency().getDenominator());
+        assertEquals(response.getWarning().getHandlingEfficiency().getNumerator(),
+                response.getAlarm().getHandlingEfficiency().getNumerator());
+    }
+
+    @Test
+    void cancelledWorkOrdersAreExcludedFromTheRateDenominator() {
+        when(adapter.load()).thenReturn(cancelledHeavyFixture());
+
+        CockpitOverviewWarningSettingsResponse response =
+                service.load("request-cancelled", "WARNING", null, null, null);
+
+        // 只有状态 7(2 张)→ 分母 0,不显示 0%,而是不适用。
+        assertEquals(Integer.valueOf(0), response.getWarning().getHandlingEfficiency().getDenominator());
+        assertEquals(Integer.valueOf(0), response.getWarning().getHandlingEfficiency().getNumerator());
+        assertEquals("EMPTY", response.getWarning().getHandlingEfficiency().getState());
+        assertEquals("NO_ELIGIBLE_WORK_ORDERS",
+                response.getWarning().getHandlingEfficiency().getReason());
     }
 
     @Test
@@ -76,7 +108,7 @@ class CockpitOverviewWarningSettingsServiceTest {
         assertEquals(Integer.valueOf(0), response.getAlarm().getHandlingEfficiency().getNumerator());
         assertEquals(Integer.valueOf(0), response.getAlarm().getHandlingEfficiency().getDenominator());
         assertEquals("EMPTY", response.getAlarm().getHandlingEfficiency().getState());
-        assertEquals("NO_EVENTS", response.getAlarm().getHandlingEfficiency().getReason());
+        assertEquals("NO_ELIGIBLE_WORK_ORDERS", response.getAlarm().getHandlingEfficiency().getReason());
     }
 
     @Test
@@ -109,7 +141,25 @@ class CockpitOverviewWarningSettingsServiceTest {
                         event("WARNING", "WRN-009", "WO-009", 9),
                         event("ALARM", "ALM-001", "WO-101", 3),
                         event("ALARM", "ALM-002", "WO-102", 5)),
-                Collections.emptyList());
+                Collections.emptyList(),
+                // 全范围工单:状态 0–7 各一张 → 分母 7(排除状态 7)、分子 1(状态 5)。
+                Arrays.asList(
+                        order(0), order(1), order(2), order(3),
+                        order(4), order(5), order(6), order(7)));
+    }
+
+    /** 只有已取消工单 → 分母 0,用于验证「不适用」而非 0%。 */
+    private static CockpitOverviewWarningSettingsSnapshot cancelledHeavyFixture() {
+        return new CockpitOverviewWarningSettingsSnapshot(
+                "2026-09-02T09:59:50+08:00",
+                Collections.singletonList(event("WARNING", "WRN-007", "WO-007", 7)),
+                Collections.emptyList(),
+                Arrays.asList(order(7), order(7)));
+    }
+
+    private static CockpitOverviewWarningSettingsSnapshot.SourceWorkOrder order(Integer state) {
+        return new CockpitOverviewWarningSettingsSnapshot.SourceWorkOrder(
+                state, "2026-09-02T09:00:00+08:00");
     }
 
     private static CockpitOverviewWarningSettingsSnapshot.SourceEvent event(

+ 63 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/BusinessCockpitEventAdapter.java

@@ -107,7 +107,24 @@ public class BusinessCockpitEventAdapter {
                 historyTypes(facts.typesById),
                 historyWarnings(facts),
                 historyAlarms(facts),
-                historyWorkOrders(facts));
+                historyWorkOrders(facts),
+                historyScopeWorkOrders(facts));
+    }
+
+    /**
+     * 范围内**全部工单**(决策票 06):历史分析汇总处置率的分母来源,
+     * 不要求事件回联;类型经设备解析供行业收敛。
+     */
+    private List<CockpitHistoryAnalysisSnapshot.SourceWorkOrder> historyScopeWorkOrders(Facts facts) {
+        List<CockpitHistoryAnalysisSnapshot.SourceWorkOrder> result = new ArrayList<>();
+        for (WorkOrderFact item : facts.workOrders) {
+            WorkOrder order = item.order;
+            result.add(new CockpitHistoryAnalysisSnapshot.SourceWorkOrder(
+                    order.getOrderStatus(),
+                    format(order.getCreateTime()),
+                    typeCodeOfDeviceId(order.getDeviceId(), facts)));
+        }
+        return result;
     }
 
     public CockpitHandlingEfficiencySnapshot loadHandlingEfficiency() {
@@ -117,7 +134,8 @@ public class BusinessCockpitEventAdapter {
                 efficiencyTypes(facts.typesById),
                 efficiencyWarnings(facts),
                 efficiencyAlarms(facts),
-                efficiencyWorkOrders(facts));
+                efficiencyWorkOrders(facts),
+                efficiencyScopeWorkOrders(facts));
     }
 
     public CockpitMultidimensionalAnalysisSnapshot loadMultidimensionalAnalysis() {
@@ -446,6 +464,33 @@ public class BusinessCockpitEventAdapter {
         return result;
     }
 
+    /**
+     * 范围内**全部工单**(决策票 06):分母要的是工单事实,不要求事件回联。
+     * 类型经 {@code work_order.device_id → equipment_base.equipment_type_id} 解析,
+     * 供服务层按行业收敛;无设备的工单仍保留(typeCode 为 null,行业筛选下会被排除)。
+     */
+    private List<CockpitHandlingEfficiencySnapshot.SourceWorkOrder> efficiencyScopeWorkOrders(Facts facts) {
+        List<CockpitHandlingEfficiencySnapshot.SourceWorkOrder> result = new ArrayList<>();
+        for (WorkOrderFact item : facts.workOrders) {
+            WorkOrder order = item.order;
+            result.add(new CockpitHandlingEfficiencySnapshot.SourceWorkOrder(
+                    trimToNull(order.getOrderNo()),
+                    order.getOrderStatus(),
+                    format(order.getCreateTime()),
+                    typeCodeOfDeviceId(order.getDeviceId(), facts)));
+        }
+        return result;
+    }
+
+    /** 工单设备 → 设备类型编码;设备缺失或类型未知时回 null。 */
+    private String typeCodeOfDeviceId(String deviceId, Facts facts) {
+        if (isBlank(deviceId) || facts.devicesById == null) {
+            return null;
+        }
+        EquipmentBase device = facts.devicesById.get(deviceId.trim());
+        return device == null ? null : typeCode(device, facts.typesById);
+    }
+
     private List<CockpitMultidimensionalAnalysisSnapshot.SourceWarning> multidimensionalWarnings(Facts facts) {
         return facts.warnings.stream()
                 .map(item -> new CockpitMultidimensionalAnalysisSnapshot.SourceWarning(
@@ -509,6 +554,7 @@ public class BusinessCockpitEventAdapter {
     private Facts loadFacts(boolean current) {
         Map<Integer, EquipmentType> types = typeIndex();
         Map<String, EquipmentBase> devices = deviceIndex();
+        Map<String, EquipmentBase> devicesById = deviceIndexById();
         Map<String, PointFact> points = pointIndex();
         Map<String, WarningType> warningTypes = warningTypeIndex();
         List<EarlyWarning> warnings = safeList(earlyWarningService.list());
@@ -559,7 +605,7 @@ public class BusinessCockpitEventAdapter {
                 .map(order -> new WorkOrderFact(order, eventNosByOrderId.getOrDefault(
                         order.getOrderId(), Collections.emptyList())))
                 .collect(Collectors.toList());
-        return new Facts(types, devices, points, warningTypes, relations, warningFacts, alarmFacts, workOrderFacts);
+        return new Facts(types, devices, devicesById, points, warningTypes, relations, warningFacts, alarmFacts, workOrderFacts);
     }
 
     private Map<Integer, EquipmentType> typeIndex() {
@@ -582,6 +628,16 @@ public class BusinessCockpitEventAdapter {
         return result;
     }
 
+    private Map<String, EquipmentBase> deviceIndexById() {
+        Map<String, EquipmentBase> result = new LinkedHashMap<>();
+        for (EquipmentBase device : safeList(equipmentBaseService.list())) {
+            if (device != null && !isBlank(device.getEquipmentId())) {
+                result.putIfAbsent(device.getEquipmentId().trim(), device);
+            }
+        }
+        return result;
+    }
+
     private Map<String, PointFact> pointIndex() {
         Map<String, PointFact> result = new LinkedHashMap<>();
         for (MonitorPoint point : safeList(monitorPointService.list())) {
@@ -820,6 +876,8 @@ public class BusinessCockpitEventAdapter {
     private static class Facts {
         private final Map<Integer, EquipmentType> typesById;
         private final Map<String, EquipmentBase> devicesByCode;
+        /** 设备主键 → 设备,供工单回联类型使用(工单存 device_id 而非设备编码)。 */
+        private final Map<String, EquipmentBase> devicesById;
         private final Map<String, PointFact> pointsByDevice;
         private final Map<String, WarningType> warningTypes;
         private final List<AlarmWarningRel> relations;
@@ -830,6 +888,7 @@ public class BusinessCockpitEventAdapter {
         private Facts(
                 Map<Integer, EquipmentType> typesById,
                 Map<String, EquipmentBase> devicesByCode,
+                Map<String, EquipmentBase> devicesById,
                 Map<String, PointFact> pointsByDevice,
                 Map<String, WarningType> warningTypes,
                 List<AlarmWarningRel> relations,
@@ -838,6 +897,7 @@ public class BusinessCockpitEventAdapter {
                 List<WorkOrderFact> workOrders) {
             this.typesById = typesById;
             this.devicesByCode = devicesByCode;
+            this.devicesById = devicesById;
             this.pointsByDevice = pointsByDevice;
             this.warningTypes = warningTypes;
             this.relations = relations;

+ 63 - 18
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHandlingEfficiencyService.java

@@ -42,9 +42,10 @@ public class CockpitHandlingEfficiencyService {
     private static final String SMART_DEVICE = "SMART_DEVICE";
     private static final String WARNING = "WARNING";
     private static final String ALARM = "ALARM";
-    private static final int CLOSED_STATE_CODE = 5;
-    private static final int CANCELLED_STATE_CODE = 7;
-    private static final String EFFICIENCY_BASIS = "CLOSED_WORK_ORDERS_OVER_TOTAL_EVENTS";
+    private static final int CLOSED_STATE_CODE = CockpitWorkOrderStateDictionary.CLOSED_RATE_CODE;
+    private static final int CANCELLED_STATE_CODE = CockpitWorkOrderStateDictionary.CANCELLED_RATE_CODE;
+    private static final String EFFICIENCY_BASIS = CockpitWorkOrderStateDictionary.HANDLING_RATE_BASIS;
+    private static final String NO_ELIGIBLE_WORK_ORDERS = "NO_ELIGIBLE_WORK_ORDERS";
     private static final long DEFAULT_WINDOW_DAYS = 7;
     private static final long MAX_WINDOW_DAYS = 31;
     private static final int DEFAULT_PAGE_SIZE = 100;
@@ -120,21 +121,25 @@ public class CockpitHandlingEfficiencyService {
                 : alarmFacts(state, types, industry, level);
         facts = inWindow(facts, window);
 
-        // Scope-wide state counts keep the distribution stable while events filter on it.
+        // 处置率口径(决策票 06 / 规格 10):分母=范围内**全部工单**(状态 0–6,
+        // 状态 7 已取消排除),不要求关联报警/事件;范围按 create_time 界定。
+        // 事件回联只决定 linkedCount / missingCount 与事件明细,不再参与分母。
         Map<Integer, Integer> stateCounts = new LinkedHashMap<>();
         for (CockpitWorkOrderState workOrderStateEntry : CockpitWorkOrderStateDictionary.states()) {
             stateCounts.put(workOrderStateEntry.getCode(), 0);
         }
+        for (CockpitHandlingEfficiencySnapshot.SourceWorkOrder order
+                : scopeWorkOrders(state, types, industry, window)) {
+            stateCounts.merge(resolveStateCode(order.getStateCode()), 1, Integer::sum);
+        }
         int linkedCount = 0;
         int missingCount = 0;
         for (EventFact fact : facts) {
-            Link link = linkOf(links, fact);
-            if (link == null) {
+            if (linkOf(links, fact) == null) {
                 missingCount++;
-                continue;
+            } else {
+                linkedCount++;
             }
-            linkedCount++;
-            stateCounts.merge(link.stateCode, 1, Integer::sum);
         }
 
         List<EventFact> filtered = new ArrayList<>();
@@ -174,7 +179,7 @@ public class CockpitHandlingEfficiencyService {
                     link != null));
         }
 
-        // Summary metrics are computed over the family/industry/level scope (pre state filter).
+        // Summary metrics: 事件计数取自事件范围,处置率取自工单范围(两者口径不同)。
         int handledCount = stateCounts.getOrDefault(CLOSED_STATE_CODE, 0);
         int cancelledCount = stateCounts.getOrDefault(CANCELLED_STATE_CODE, 0);
         int inProgressCount = 0;
@@ -183,7 +188,13 @@ public class CockpitHandlingEfficiencyService {
                 inProgressCount += entry.getValue();
             }
         }
-        int scopeTotal = facts.size();
+        int totalEvents = facts.size();
+        int eligibleWorkOrders = 0;
+        for (Map.Entry<Integer, Integer> entry : stateCounts.entrySet()) {
+            if (CockpitWorkOrderStateDictionary.isRateEligible(entry.getKey())) {
+                eligibleWorkOrders += entry.getValue();
+            }
+        }
 
         List<BigDecimal> closedDurations = new ArrayList<>();
         for (EventFact fact : facts) {
@@ -223,29 +234,35 @@ public class CockpitHandlingEfficiencyService {
                 new CockpitHandlingEfficiencyResponse.MapFilter(
                         family, workOrderState, sorted(deviceCodes)),
                 new CockpitHandlingEfficiencyResponse.Summary(
-                        scopeTotal,
+                        totalEvents,
                         linkedCount,
                         missingCount,
                         handledCount,
                         inProgressCount,
                         cancelledCount,
-                        efficiency(handledCount, scopeTotal),
+                        efficiency(handledCount, eligibleWorkOrders),
                         averageDuration(closedDurations)),
                 stateRows,
                 durationTrend(state, family, types, industry, level, links, window),
                 items);
     }
 
+    /**
+     * 处置率=状态 5 ÷ 状态 0–6 工单数(决策票 06)。分母为 0 → {@code state=EMPTY}
+     * 且 {@code reason=NO_ELIGIBLE_WORK_ORDERS},前端据此显示「不适用」而非 0%。
+     */
     private static CockpitHandlingEfficiencyResponse.HandlingEfficiency efficiency(
-            int handled, int total) {
-        if (total <= 0) {
+            int handled, int eligibleWorkOrders) {
+        if (eligibleWorkOrders <= 0) {
             return new CockpitHandlingEfficiencyResponse.HandlingEfficiency(
-                    BigDecimal.ZERO.setScale(4), handled, total, "EMPTY", "NO_EVENTS", EFFICIENCY_BASIS);
+                    BigDecimal.ZERO.setScale(4), handled, eligibleWorkOrders,
+                    "EMPTY", NO_ELIGIBLE_WORK_ORDERS, EFFICIENCY_BASIS);
         }
         return new CockpitHandlingEfficiencyResponse.HandlingEfficiency(
-                BigDecimal.valueOf(handled).divide(BigDecimal.valueOf(total), 4, RoundingMode.HALF_UP),
+                BigDecimal.valueOf(handled)
+                        .divide(BigDecimal.valueOf(eligibleWorkOrders), 4, RoundingMode.HALF_UP),
                 handled,
-                total,
+                eligibleWorkOrders,
                 "AVAILABLE",
                 null,
                 EFFICIENCY_BASIS);
@@ -408,6 +425,34 @@ public class CockpitHandlingEfficiencyService {
         return result;
     }
 
+    /**
+     * 范围内**全部工单**(不要求关联报警/事件),用于处置率分母:
+     * 按 {@code create_time} 落在窗口内、按行业(经设备类型)收敛,状态 7 由调用方排除。
+     */
+    private static List<CockpitHandlingEfficiencySnapshot.SourceWorkOrder> scopeWorkOrders(
+            CockpitHandlingEfficiencySnapshot snapshot,
+            Map<String, CockpitHandlingEfficiencySnapshot.SourceType> types,
+            String industry,
+            TimeWindow window) {
+        List<CockpitHandlingEfficiencySnapshot.SourceWorkOrder> result = new ArrayList<>();
+        if (snapshot.getScopeWorkOrders() == null) {
+            return result;
+        }
+        for (CockpitHandlingEfficiencySnapshot.SourceWorkOrder order : snapshot.getScopeWorkOrders()) {
+            if (order == null || !inScope(types, order.getTypeCode(), industry)) {
+                continue;
+            }
+            OffsetDateTime createTime = parseEventTime(order.getCreateTime());
+            if (createTime == null
+                    || createTime.isBefore(window.start)
+                    || createTime.isAfter(window.end)) {
+                continue;
+            }
+            result.add(order);
+        }
+        return result;
+    }
+
     private static Map<String, Link> links(
             List<CockpitHandlingEfficiencySnapshot.SourceWorkOrderLink> source) {
         Map<String, Link> result = new LinkedHashMap<>();

+ 38 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHandlingEfficiencySnapshot.java

@@ -9,6 +9,10 @@ import java.util.List;
  * <p>Delivers both event families separately plus their work-order links. Links
  * are nullable per event: an event without a link is counted as missing while
  * keeping the event in the result.
+ *
+ * <p>{@link #scopeWorkOrders} 是**范围内全部工单**(不要求关联报警/事件),
+ * 供处置率分母(状态 0–6 工单数)使用;{@link #workOrders} 仍只承载事件回联,
+ * 两者口径不同,不得互相替代。
  */
 public class CockpitHandlingEfficiencySnapshot {
     private final String updatedAt;
@@ -16,6 +20,7 @@ public class CockpitHandlingEfficiencySnapshot {
     private final List<SourceWarning> warnings;
     private final List<SourceAlarm> alarms;
     private final List<SourceWorkOrderLink> workOrders;
+    private final List<SourceWorkOrder> scopeWorkOrders;
 
     public CockpitHandlingEfficiencySnapshot(
             String updatedAt,
@@ -23,11 +28,22 @@ public class CockpitHandlingEfficiencySnapshot {
             List<SourceWarning> warnings,
             List<SourceAlarm> alarms,
             List<SourceWorkOrderLink> workOrders) {
+        this(updatedAt, types, warnings, alarms, workOrders, Collections.emptyList());
+    }
+
+    public CockpitHandlingEfficiencySnapshot(
+            String updatedAt,
+            List<SourceType> types,
+            List<SourceWarning> warnings,
+            List<SourceAlarm> alarms,
+            List<SourceWorkOrderLink> workOrders,
+            List<SourceWorkOrder> scopeWorkOrders) {
         this.updatedAt = updatedAt;
         this.types = types;
         this.warnings = warnings;
         this.alarms = alarms;
         this.workOrders = workOrders;
+        this.scopeWorkOrders = scopeWorkOrders;
     }
 
     public static CockpitHandlingEfficiencySnapshot empty(String updatedAt) {
@@ -36,6 +52,7 @@ public class CockpitHandlingEfficiencySnapshot {
                 Collections.emptyList(),
                 Collections.emptyList(),
                 Collections.emptyList(),
+                Collections.emptyList(),
                 Collections.emptyList());
     }
 
@@ -44,6 +61,27 @@ public class CockpitHandlingEfficiencySnapshot {
     public List<SourceWarning> getWarnings() { return warnings; }
     public List<SourceAlarm> getAlarms() { return alarms; }
     public List<SourceWorkOrderLink> getWorkOrders() { return workOrders; }
+    public List<SourceWorkOrder> getScopeWorkOrders() { return scopeWorkOrders; }
+
+    /** 范围内单张工单的处置率事实:状态、创建时间与类型(用于行业范围判定)。 */
+    public static class SourceWorkOrder {
+        private final String orderNo;
+        private final Integer stateCode;
+        private final String createTime;
+        private final String typeCode;
+
+        public SourceWorkOrder(String orderNo, Integer stateCode, String createTime, String typeCode) {
+            this.orderNo = orderNo;
+            this.stateCode = stateCode;
+            this.createTime = createTime;
+            this.typeCode = typeCode;
+        }
+
+        public String getOrderNo() { return orderNo; }
+        public Integer getStateCode() { return stateCode; }
+        public String getCreateTime() { return createTime; }
+        public String getTypeCode() { return typeCode; }
+    }
 
     public static class SourceType {
         private final String code;

+ 69 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHistoryAnalysisService.java

@@ -3,6 +3,7 @@ package com.zksy.cockpit.event;
 import com.zksy.cockpit.foundation.CockpitErrorCategory;
 import com.zksy.cockpit.foundation.CockpitErrorPayload;
 import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
+import com.zksy.cockpit.workorder.CockpitWorkOrderStateDictionary;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
@@ -40,7 +41,7 @@ public class CockpitHistoryAnalysisService {
     private static final String SMART_DEVICE = "SMART_DEVICE";
     private static final String WARNING = "WARNING";
     private static final String ALARM = "ALARM";
-    private static final int HANDLED_STATE_CODE = 5;
+    private static final int HANDLED_STATE_CODE = CockpitWorkOrderStateDictionary.CLOSED_RATE_CODE;
     private static final long DEFAULT_WINDOW_DAYS = 7;
     private static final long MAX_WINDOW_DAYS = 31;
     private static final DateTimeFormatter TIMESTAMP =
@@ -158,6 +159,9 @@ public class CockpitHistoryAnalysisService {
         }
 
         String updatedAt = isBlank(state.getUpdatedAt()) ? businessTime() : state.getUpdatedAt();
+        // 汇总处置率=范围内全部工单(状态 0–6)口径(决策票 06),与行级事件桶分开;
+        // 行级桶仍是事件时间桶,其 efficiency 保留事件口径。
+        ScopeRate scopeRate = scopeRate(state, types, industry, window);
         return new CockpitHistoryAnalysisResponse(
                 requestId,
                 "v1",
@@ -173,8 +177,9 @@ public class CockpitHistoryAnalysisService {
                 new CockpitHistoryAnalysisResponse.Filters(family, level, industry),
                 new CockpitHistoryAnalysisResponse.Summary(
                         totalEvents,
+                        // 保持事件口径,与行级 handledCount 同维(决策 q7:只改汇总处置率)。
                         totalHandled,
-                        efficiency(totalHandled, totalEvents)),
+                        scopeEfficiency(scopeRate.handled, scopeRate.eligible)),
                 rows);
     }
 
@@ -239,6 +244,68 @@ public class CockpitHistoryAnalysisService {
         return industry == null || industry.equals(type.getIndustry());
     }
 
+    /**
+     * 范围处置率:分母=窗口内全部工单的状态 0–6 计数(状态 7 已取消排除),
+     * 按 {@code create_time} 界定窗口、按行业(经类型)收敛,与事件是否回联无关。
+     */
+    private static ScopeRate scopeRate(
+            CockpitHistoryAnalysisSnapshot snapshot,
+            Map<String, CockpitHistoryAnalysisSnapshot.SourceType> types,
+            String industry,
+            TimeWindow window) {
+        int handled = 0;
+        int eligible = 0;
+        if (snapshot.getScopeWorkOrders() == null) {
+            return new ScopeRate(0, 0);
+        }
+        for (CockpitHistoryAnalysisSnapshot.SourceWorkOrder order : snapshot.getScopeWorkOrders()) {
+            if (order == null || !inScope(types, order.getTypeCode(), industry)) {
+                continue;
+            }
+            OffsetDateTime createTime = parseEventTime(order.getCreateTime());
+            if (createTime == null
+                    || createTime.isBefore(window.start)
+                    || createTime.isAfter(window.end)) {
+                continue;
+            }
+            Integer stateCode = order.getStateCode();
+            if (!CockpitWorkOrderStateDictionary.isRateEligible(stateCode)) {
+                continue;
+            }
+            eligible++;
+            if (stateCode != null && stateCode == HANDLED_STATE_CODE) {
+                handled++;
+            }
+        }
+        return new ScopeRate(handled, eligible);
+    }
+
+    private static CockpitHistoryAnalysisResponse.HandlingEfficiency scopeEfficiency(
+            int handled, int eligible) {
+        if (eligible <= 0) {
+            return new CockpitHistoryAnalysisResponse.HandlingEfficiency(
+                    BigDecimal.ZERO.setScale(4), handled, eligible, "EMPTY", "NO_ELIGIBLE_WORK_ORDERS");
+        }
+        return new CockpitHistoryAnalysisResponse.HandlingEfficiency(
+                BigDecimal.valueOf(handled)
+                        .divide(BigDecimal.valueOf(eligible), 4, RoundingMode.HALF_UP),
+                handled,
+                eligible,
+                "AVAILABLE",
+                null);
+    }
+
+    /** 范围处置率读数:分子(状态 5)与分母(状态 0–6)。 */
+    private static class ScopeRate {
+        private final int handled;
+        private final int eligible;
+
+        private ScopeRate(int handled, int eligible) {
+            this.handled = handled;
+            this.eligible = eligible;
+        }
+    }
+
     private static Map<String, Integer> handledLinks(
             List<CockpitHistoryAnalysisSnapshot.SourceWorkOrderLink> links) {
         Map<String, Integer> result = new LinkedHashMap<>();

+ 45 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHistoryAnalysisSnapshot.java

@@ -16,6 +16,7 @@ public class CockpitHistoryAnalysisSnapshot {
     private final List<SourceWarning> warnings;
     private final List<SourceAlarm> alarms;
     private final List<SourceWorkOrderLink> workOrders;
+    private final List<SourceWorkOrder> scopeWorkOrders;
 
     public CockpitHistoryAnalysisSnapshot(
             String updatedAt,
@@ -23,11 +24,22 @@ public class CockpitHistoryAnalysisSnapshot {
             List<SourceWarning> warnings,
             List<SourceAlarm> alarms,
             List<SourceWorkOrderLink> workOrders) {
+        this(updatedAt, types, warnings, alarms, workOrders, Collections.emptyList());
+    }
+
+    public CockpitHistoryAnalysisSnapshot(
+            String updatedAt,
+            List<SourceType> types,
+            List<SourceWarning> warnings,
+            List<SourceAlarm> alarms,
+            List<SourceWorkOrderLink> workOrders,
+            List<SourceWorkOrder> scopeWorkOrders) {
         this.updatedAt = updatedAt;
         this.types = types;
         this.warnings = warnings;
         this.alarms = alarms;
         this.workOrders = workOrders;
+        this.scopeWorkOrders = scopeWorkOrders;
     }
 
     public static CockpitHistoryAnalysisSnapshot empty(String updatedAt) {
@@ -36,6 +48,7 @@ public class CockpitHistoryAnalysisSnapshot {
                 Collections.emptyList(),
                 Collections.emptyList(),
                 Collections.emptyList(),
+                Collections.emptyList(),
                 Collections.emptyList());
     }
 
@@ -59,6 +72,38 @@ public class CockpitHistoryAnalysisSnapshot {
         return workOrders;
     }
 
+    /**
+     * 范围内**全部工单**(决策票 06):处置率分母=状态 0–6 工单数,不要求事件回联;
+     * 按 {@code create_time} 界定窗口。行级小时桶仍按事件时间,两者口径不同。
+     */
+    public List<SourceWorkOrder> getScopeWorkOrders() {
+        return scopeWorkOrders;
+    }
+
+    public static class SourceWorkOrder {
+        private final Integer stateCode;
+        private final String createTime;
+        private final String typeCode;
+
+        public SourceWorkOrder(Integer stateCode, String createTime, String typeCode) {
+            this.stateCode = stateCode;
+            this.createTime = createTime;
+            this.typeCode = typeCode;
+        }
+
+        public Integer getStateCode() {
+            return stateCode;
+        }
+
+        public String getCreateTime() {
+            return createTime;
+        }
+
+        public String getTypeCode() {
+            return typeCode;
+        }
+    }
+
     public static class SourceType {
         private final String code;
         private final String name;

+ 20 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/overview/BusinessCockpitOverviewWarningSettingsAdapter.java

@@ -155,7 +155,26 @@ public class BusinessCockpitOverviewWarningSettingsAdapter
         addBucket(trendPoints, "WARNING", bucketFor(warnings, alarms, warningNosByAlarmId, orderByOrderId));
         addBucket(trendPoints, "ALARM", bucketFor(alarms, orderByOrderId));
 
-        return new CockpitOverviewWarningSettingsSnapshot(offsetTime(updatedAt), events, trendPoints);
+        return new CockpitOverviewWarningSettingsSnapshot(
+                offsetTime(updatedAt), events, trendPoints, scopeWorkOrders(orders));
+    }
+
+    /**
+     * 范围内**全部工单**(决策票 06):处置率分母要的是工单事实,不要求事件回联。
+     * 状态码原样透出(是否计入分母由服务层的 0–6 口径判定)。
+     */
+    private static List<CockpitOverviewWarningSettingsSnapshot.SourceWorkOrder> scopeWorkOrders(
+            List<WorkOrder> orders) {
+        List<CockpitOverviewWarningSettingsSnapshot.SourceWorkOrder> result = new ArrayList<>();
+        for (WorkOrder order : orders) {
+            if (order == null) {
+                continue;
+            }
+            result.add(new CockpitOverviewWarningSettingsSnapshot.SourceWorkOrder(
+                    order.getOrderStatus(),
+                    offsetTime(toLocal(order.getCreateTime()))));
+        }
+        return result;
     }
 
     private Map<String, Counter> bucketFor(

+ 86 - 19
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/overview/CockpitOverviewWarningSettingsService.java

@@ -34,7 +34,7 @@ public class CockpitOverviewWarningSettingsService {
             CockpitWorkOrderStateDictionary.states().stream()
                     .map(CockpitWorkOrderState::getCode)
                     .collect(Collectors.toList());
-    private static final int HANDLED_STATE = 5;
+    private static final int HANDLED_STATE = CockpitWorkOrderStateDictionary.CLOSED_RATE_CODE;
     private static final DateTimeFormatter BUCKET_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
 
     private final CockpitOverviewWarningSettingsAdapter adapter;
@@ -61,6 +61,7 @@ public class CockpitOverviewWarningSettingsService {
         String freshnessStatus = hasSnapshot ? "CURRENT" : "EMPTY";
         String freshnessReason = hasSnapshot ? "READ_ONLY_AGGREGATE" : "NO_SNAPSHOT";
 
+        ScopeRate scopeRate = scopeRate(snapshot, window);
         return new CockpitOverviewWarningSettingsResponse(
                 requestId,
                 "v1",
@@ -78,8 +79,8 @@ public class CockpitOverviewWarningSettingsService {
                         freshnessReason,
                         updatedAt,
                         businessTime()),
-                familyMetrics(snapshot, "WARNING", window),
-                familyMetrics(snapshot, "ALARM", window),
+                familyMetrics(snapshot, "WARNING", window, scopeRate),
+                familyMetrics(snapshot, "ALARM", window, scopeRate),
                 mapFilter(selectedMode));
     }
 
@@ -159,7 +160,8 @@ public class CockpitOverviewWarningSettingsService {
     private CockpitOverviewWarningSettingsResponse.FamilyMetrics familyMetrics(
             CockpitOverviewWarningSettingsSnapshot snapshot,
             String eventFamily,
-            Window window) {
+            Window window,
+            ScopeRate scopeRate) {
         List<CockpitOverviewWarningSettingsSnapshot.SourceEvent> sourceEvents =
                 snapshot == null || snapshot.getEvents() == null
                         ? Collections.emptyList()
@@ -192,7 +194,6 @@ public class CockpitOverviewWarningSettingsService {
                 continue;
             }
             total++;
-            workOrderStates.merge(state, 1, Integer::sum);
             if (state == HANDLED_STATE) {
                 handled++;
             } else if (state == 7) {
@@ -202,18 +203,10 @@ public class CockpitOverviewWarningSettingsService {
             }
         }
 
-        int efficiencyDenominator = total;
-        BigDecimal value = efficiencyDenominator == 0
-                ? BigDecimal.ZERO.setScale(4)
-                : BigDecimal.valueOf(handled)
-                        .divide(BigDecimal.valueOf(efficiencyDenominator), 4, RoundingMode.HALF_UP);
-        CockpitOverviewWarningSettingsResponse.Efficiency efficiency =
-                new CockpitOverviewWarningSettingsResponse.Efficiency(
-                        handled,
-                        efficiencyDenominator,
-                        value,
-                        efficiencyDenominator == 0 ? "EMPTY" : "AVAILABLE",
-                        efficiencyDenominator == 0 ? "NO_EVENTS" : null);
+        // 处置率=范围内全部工单(状态 0–6)口径,两个 family 共用同一份范围读数
+        // (决策票 06:分母不要求关联报警/事件)。事件计数仍按 family 各自统计。
+        int rateNumerator = scopeRate.handled;
+        int rateDenominator = scopeRate.eligible;
 
         return new CockpitOverviewWarningSettingsResponse.FamilyMetrics(
                 eventFamily,
@@ -225,8 +218,8 @@ public class CockpitOverviewWarningSettingsService {
                 cancelledCount,
                 unlinkedEventCount,
                 invalidWorkOrderStateCount,
-                efficiency,
-                workOrderStates,
+                scopeEfficiency(rateNumerator, rateDenominator),
+                scopeRate.stateCounts,
                 trend(snapshot, eventFamily, window));
     }
 
@@ -257,6 +250,80 @@ public class CockpitOverviewWarningSettingsService {
         return trend;
     }
 
+    /**
+     * 范围处置率:分母=窗口内全部工单的状态 0–6 计数(状态 7 已取消排除),
+     * 按 {@code create_time} 界定窗口。两 family 共用同一份读数。
+     */
+    private ScopeRate scopeRate(CockpitOverviewWarningSettingsSnapshot snapshot, Window window) {
+        Map<Integer, Integer> stateCounts = new LinkedHashMap<>();
+        for (Integer state : WORK_ORDER_STATES) {
+            stateCounts.put(state, 0);
+        }
+        List<CockpitOverviewWarningSettingsSnapshot.SourceWorkOrder> orders =
+                snapshot == null || snapshot.getScopeWorkOrders() == null
+                        ? Collections.emptyList()
+                        : snapshot.getScopeWorkOrders();
+        for (CockpitOverviewWarningSettingsSnapshot.SourceWorkOrder order : orders) {
+            if (order == null || !inWindow(order.getCreateTime(), window)) {
+                continue;
+            }
+            Integer state = order.getStateCode();
+            if (state == null || !WORK_ORDER_STATES.contains(state)) {
+                continue;
+            }
+            stateCounts.merge(state, 1, Integer::sum);
+        }
+        int eligible = 0;
+        for (Map.Entry<Integer, Integer> entry : stateCounts.entrySet()) {
+            if (CockpitWorkOrderStateDictionary.isRateEligible(entry.getKey())) {
+                eligible += entry.getValue();
+            }
+        }
+        return new ScopeRate(stateCounts.getOrDefault(HANDLED_STATE, 0), eligible, stateCounts);
+    }
+
+    /** 无界窗口(`CURRENT_BASELINE`)覆盖全部工单;有界时按 create_time 判定。 */
+    private static boolean inWindow(String createTime, Window window) {
+        if (!window.isBounded()) {
+            return true;
+        }
+        if (isBlank(createTime)) {
+            return false;
+        }
+        try {
+            OffsetDateTime parsed = OffsetDateTime.parse(createTime);
+            return !parsed.isBefore(window.start) && !parsed.isAfter(window.end);
+        } catch (DateTimeParseException ignored) {
+            return false;
+        }
+    }
+
+    private static CockpitOverviewWarningSettingsResponse.Efficiency scopeEfficiency(int handled, int eligible) {
+        BigDecimal value = eligible == 0
+                ? BigDecimal.ZERO.setScale(4)
+                : BigDecimal.valueOf(handled)
+                        .divide(BigDecimal.valueOf(eligible), 4, RoundingMode.HALF_UP);
+        return new CockpitOverviewWarningSettingsResponse.Efficiency(
+                handled,
+                eligible,
+                value,
+                eligible == 0 ? "EMPTY" : "AVAILABLE",
+                eligible == 0 ? "NO_ELIGIBLE_WORK_ORDERS" : null);
+    }
+
+    /** 范围处置率读数:分子(状态 5)、分母(状态 0–6)、逐态分布。 */
+    private static class ScopeRate {
+        private final int handled;
+        private final int eligible;
+        private final Map<Integer, Integer> stateCounts;
+
+        private ScopeRate(int handled, int eligible, Map<Integer, Integer> stateCounts) {
+            this.handled = handled;
+            this.eligible = eligible;
+            this.stateCounts = stateCounts;
+        }
+    }
+
     private static CockpitOverviewWarningSettingsResponse.Efficiency efficiency(int total, int handled) {
         BigDecimal value = total == 0
                 ? BigDecimal.ZERO.setScale(4)

+ 29 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/overview/CockpitOverviewWarningSettingsSnapshot.java

@@ -6,20 +6,49 @@ public class CockpitOverviewWarningSettingsSnapshot {
     private final String updatedAt;
     private final List<SourceEvent> events;
     private final List<SourceTrendPoint> trendPoints;
+    private final List<SourceWorkOrder> scopeWorkOrders;
 
     public CockpitOverviewWarningSettingsSnapshot(
             String updatedAt,
             List<SourceEvent> events,
             List<SourceTrendPoint> trendPoints) {
+        this(updatedAt, events, trendPoints, java.util.Collections.emptyList());
+    }
+
+    public CockpitOverviewWarningSettingsSnapshot(
+            String updatedAt,
+            List<SourceEvent> events,
+            List<SourceTrendPoint> trendPoints,
+            List<SourceWorkOrder> scopeWorkOrders) {
         this.updatedAt = updatedAt;
         this.events = events;
         this.trendPoints = trendPoints;
+        this.scopeWorkOrders = scopeWorkOrders;
     }
 
     public String getUpdatedAt() { return updatedAt; }
     public List<SourceEvent> getEvents() { return events; }
     public List<SourceTrendPoint> getTrendPoints() { return trendPoints; }
 
+    /**
+     * 范围内**全部工单**(决策票 06):处置率分母=状态 0–6 工单数,不要求事件回联;
+     * 按 {@code create_time} 界定窗口。事件趋势仍走 {@link SourceTrendPoint}。
+     */
+    public List<SourceWorkOrder> getScopeWorkOrders() { return scopeWorkOrders; }
+
+    public static class SourceWorkOrder {
+        private final Integer stateCode;
+        private final String createTime;
+
+        public SourceWorkOrder(Integer stateCode, String createTime) {
+            this.stateCode = stateCode;
+            this.createTime = createTime;
+        }
+
+        public Integer getStateCode() { return stateCode; }
+        public String getCreateTime() { return createTime; }
+    }
+
     public static class SourceEvent {
         private final String eventFamily;
         private final String eventCode;

+ 24 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderStateDictionary.java

@@ -14,6 +14,22 @@ import java.util.Map;
  * 不再作为对外编码。
  */
 public final class CockpitWorkOrderStateDictionary {
+
+    /**
+     * 处置率口径的唯一权威来源(决策票 06 / 规格 10)。
+     *
+     * <p>分子=状态 5(已结案);分母=状态 0–6 工单数之和(状态 7 取消排除);
+     * 范围=当前筛选内**全部工单**,不要求关联报警/事件。
+     */
+    public static final String HANDLING_RATE_BASIS =
+            "CLOSED_WORK_ORDERS_OVER_ELIGIBLE_WORK_ORDERS";
+
+    /** 处置率分子:已结案。 */
+    public static final int CLOSED_RATE_CODE = 5;
+
+    /** 处置率排除的终态:已取消。 */
+    public static final int CANCELLED_RATE_CODE = 7;
+
     private static final List<CockpitWorkOrderState> STATES = Collections.unmodifiableList(Arrays.asList(
             new CockpitWorkOrderState(0, "待派单"),
             new CockpitWorkOrderState(1, "待接单"),
@@ -44,6 +60,14 @@ public final class CockpitWorkOrderStateDictionary {
         return code == null ? null : BY_CODE.get(code);
     }
 
+    /**
+     * 该状态是否计入处置率分母(状态 0–6)。状态 7 已取消排除;未知码不计入,
+     * 避免脏值把分母抬高。
+     */
+    public static boolean isRateEligible(Integer code) {
+        return code != null && code >= 0 && code < CANCELLED_RATE_CODE;
+    }
+
     /** 将工单操作日志操作码映射为操作完成后的当前运行时状态。 */
     public static CockpitWorkOrderState byOperationCode(Integer operationCode, Integer currentStateCode) {
         if (operationCode == null) {