Преглед изворни кода

feat(cockpit): district on event family endpoints and industry on work-orders

- events/current、events/history-analysis、events/handling-efficiency 补 district
  (0/1/2/UNASSIGNED;非法值 400;filters 回显 district/districtName,双字段口径)
- 事件族三端点的工单口径(处置率分母、状态分布、工单范围)与事件同片区收敛:
  只筛事件会让同一响应内两个口径打架(票 67 反馈的「各说各话」)
- /work-orders 补 industry(工单设备→设备类型树→行业派生,非法值 400)
- 契约测试覆盖:合法值生效 / 非法值 400 / 不传=全域;全量 Cockpit*Test 576/576
codex пре 1 недеља
родитељ
комит
6c3c889809
24 измењених фајлова са 700 додато и 81 уклоњено
  1. 3 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitCurrentEventsController.java
  2. 3 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitHandlingEfficiencyController.java
  3. 3 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitHistoryAnalysisController.java
  4. 12 4
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitWorkOrderReadController.java
  5. 2 1
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitWorkOrderReadAdapterTest.java
  6. 105 5
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitCurrentEventsControllerTest.java
  7. 117 4
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitHandlingEfficiencyControllerTest.java
  8. 99 12
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitHistoryAnalysisControllerTest.java
  9. 1 1
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitWorkOrderAssociationContractTest.java
  10. 57 1
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitWorkOrderListContractTest.java
  11. 25 8
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/BusinessCockpitEventAdapter.java
  12. 10 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitCurrentEventsResponse.java
  13. 26 5
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitCurrentEventsService.java
  14. 20 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitCurrentEventsSnapshot.java
  15. 10 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHandlingEfficiencyResponse.java
  16. 32 13
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHandlingEfficiencyService.java
  17. 19 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHandlingEfficiencySnapshot.java
  18. 10 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHistoryAnalysisResponse.java
  19. 29 10
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHistoryAnalysisService.java
  20. 31 5
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHistoryAnalysisSnapshot.java
  21. 45 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/BusinessCockpitWorkOrderReadAdapter.java
  22. 5 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderReadAdapter.java
  23. 35 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderReadService.java
  24. 1 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/EmptyCockpitWorkOrderReadAdapter.java

+ 3 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitCurrentEventsController.java

@@ -33,6 +33,8 @@ public class CockpitCurrentEventsController {
             @RequestParam(value = "level", required = false) String level,
             @RequestParam(value = "level", required = false) String level,
             @RequestParam(value = "industry", required = false) String industry,
             @RequestParam(value = "industry", required = false) String industry,
             @RequestParam(value = "status", required = false) String status,
             @RequestParam(value = "status", required = false) String status,
+            // 范围维度=片区(票 77):0/1/2 按片区筛、UNASSIGNED 只看未分区、不传=不筛。
+            @RequestParam(value = "district", required = false) String district,
             @RequestParam(value = "page", defaultValue = "1") Integer page,
             @RequestParam(value = "page", defaultValue = "1") Integer page,
             @RequestParam(value = "pageSize", defaultValue = "100") Integer pageSize,
             @RequestParam(value = "pageSize", defaultValue = "100") Integer pageSize,
             @RequestHeader(value = REQUEST_ID_HEADER, required = false) String requestId) {
             @RequestHeader(value = REQUEST_ID_HEADER, required = false) String requestId) {
@@ -43,6 +45,7 @@ public class CockpitCurrentEventsController {
                         level,
                         level,
                         industry,
                         industry,
                         status,
                         status,
+                        district,
                         page,
                         page,
                         pageSize)));
                         pageSize)));
     }
     }

+ 3 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitHandlingEfficiencyController.java

@@ -34,6 +34,8 @@ public class CockpitHandlingEfficiencyController {
             @RequestParam(value = "industry", required = false) String industry,
             @RequestParam(value = "industry", required = false) String industry,
             @RequestParam(value = "level", required = false) String level,
             @RequestParam(value = "level", required = false) String level,
             @RequestParam(value = "workOrderState", required = false) String workOrderState,
             @RequestParam(value = "workOrderState", required = false) String workOrderState,
+            // 范围维度=片区(票 77):语义与事件族其它端点同套(0/1/2/UNASSIGNED,非法 400)。
+            @RequestParam(value = "district", required = false) String district,
             @RequestParam(value = "startTime", required = false) String startTime,
             @RequestParam(value = "startTime", required = false) String startTime,
             @RequestParam(value = "endTime", required = false) String endTime,
             @RequestParam(value = "endTime", required = false) String endTime,
             @RequestParam(value = "page", defaultValue = "1") Integer page,
             @RequestParam(value = "page", defaultValue = "1") Integer page,
@@ -46,6 +48,7 @@ public class CockpitHandlingEfficiencyController {
                         industry,
                         industry,
                         level,
                         level,
                         workOrderState,
                         workOrderState,
+                        district,
                         startTime,
                         startTime,
                         endTime,
                         endTime,
                         page,
                         page,

+ 3 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitHistoryAnalysisController.java

@@ -32,6 +32,8 @@ public class CockpitHistoryAnalysisController {
             @RequestParam(value = "eventFamily", required = false) String eventFamily,
             @RequestParam(value = "eventFamily", required = false) String eventFamily,
             @RequestParam(value = "level", required = false) String level,
             @RequestParam(value = "level", required = false) String level,
             @RequestParam(value = "industry", required = false) String industry,
             @RequestParam(value = "industry", required = false) String industry,
+            // 范围维度=片区(票 77):语义与事件族其它端点同套(0/1/2/UNASSIGNED,非法 400)。
+            @RequestParam(value = "district", required = false) String district,
             @RequestParam(value = "startTime", required = false) String startTime,
             @RequestParam(value = "startTime", required = false) String startTime,
             @RequestParam(value = "endTime", required = false) String endTime,
             @RequestParam(value = "endTime", required = false) String endTime,
             @RequestParam(value = "page", defaultValue = "1") Integer page,
             @RequestParam(value = "page", defaultValue = "1") Integer page,
@@ -43,6 +45,7 @@ public class CockpitHistoryAnalysisController {
                         eventFamily,
                         eventFamily,
                         level,
                         level,
                         industry,
                         industry,
+                        district,
                         startTime,
                         startTime,
                         endTime,
                         endTime,
                         page,
                         page,

+ 12 - 4
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/cockpit/CockpitWorkOrderReadController.java

@@ -77,7 +77,10 @@ public class CockpitWorkOrderReadController {
      *
      *
      * <p>查询参数都可缺省:`page` 默认 1、`pageSize` 默认 10(上限 100)、
      * <p>查询参数都可缺省:`page` 默认 1、`pageSize` 默认 10(上限 100)、
      * `stateCode` 缺省不筛、`startTime`/`endTime`(ISO-8601 带偏移)缺省**不限时间**。
      * `stateCode` 缺省不筛、`startTime`/`endTime`(ISO-8601 带偏移)缺省**不限时间**。
-     * 夹取与时间解析都在 service,控制器不做第二套校验。
+     * 夹取、时间解析与行业校验都在 service,控制器不做第二套校验。
+     *
+     * <p>industry 是票 77 补的:口径经「工单设备 → 设备类型 → 行业」派生,
+     * 与 handling-efficiency 的工单范围同一套规则(列表与状态分布必须筛同一批工单)。
      *
      *
      * <p>`startTime`/`endTime` 是 2026-10-22 维护者反馈「工单列表没按时间筛」后补的:
      * <p>`startTime`/`endTime` 是 2026-10-22 维护者反馈「工单列表没按时间筛」后补的:
      * 面板按近 7/30/90 天切换时,列表必须跟着收窄(否则上方的状态分布按窗口统计、
      * 面板按近 7/30/90 天切换时,列表必须跟着收窄(否则上方的状态分布按窗口统计、
@@ -88,18 +91,23 @@ public class CockpitWorkOrderReadController {
             @RequestParam(value = "page", required = false) Integer page,
             @RequestParam(value = "page", required = false) Integer page,
             @RequestParam(value = "pageSize", required = false) Integer pageSize,
             @RequestParam(value = "pageSize", required = false) Integer pageSize,
             @RequestParam(value = "stateCode", required = false) Integer stateCode,
             @RequestParam(value = "stateCode", required = false) Integer stateCode,
+            // 行业(票 77):GAS/WATER_SUPPLY/DRAINAGE/MANHOLE,不传=全域,非法值 400。
+            @RequestParam(value = "industry", required = false) String industry,
             @RequestParam(value = "startTime", required = false) String startTime,
             @RequestParam(value = "startTime", required = false) String startTime,
             @RequestParam(value = "endTime", required = false) String endTime,
             @RequestParam(value = "endTime", required = false) String endTime,
             @RequestHeader(value = REQUEST_ID_HEADER, required = false) String requestId) {
             @RequestHeader(value = REQUEST_ID_HEADER, required = false) String requestId) {
         CockpitWorkOrderListResponse response = workOrderService.loadAll(
         CockpitWorkOrderListResponse response = workOrderService.loadAll(
-                resolveRequestId(requestId), page, pageSize, stateCode, startTime, endTime);
+                resolveRequestId(requestId), page, pageSize, stateCode, industry, startTime, endTime);
         return ResponseEntity.ok(AjaxResult.success(response));
         return ResponseEntity.ok(AjaxResult.success(response));
     }
     }
 
 
     @ExceptionHandler(CockpitInvalidRequestException.class)
     @ExceptionHandler(CockpitInvalidRequestException.class)
-    public ResponseEntity<AjaxResult> handleInvalidRequest(HttpServletRequest request) {
+    public ResponseEntity<AjaxResult> handleInvalidRequest(
+            HttpServletRequest request,
+            CockpitInvalidRequestException exception) {
         return error(HttpStatus.BAD_REQUEST, workOrderService.invalidRequest(
         return error(HttpStatus.BAD_REQUEST, workOrderService.invalidRequest(
-                resolveRequestId(request.getHeader(REQUEST_ID_HEADER))));
+                resolveRequestId(request.getHeader(REQUEST_ID_HEADER)),
+                exception.getMessage()));
     }
     }
 
 
     @ExceptionHandler(CockpitDeviceNotFoundException.class)
     @ExceptionHandler(CockpitDeviceNotFoundException.class)

+ 2 - 1
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitWorkOrderReadAdapterTest.java

@@ -70,7 +70,8 @@ class BusinessCockpitWorkOrderReadAdapterTest {
                 mock(EarlyWarningService.class),
                 mock(EarlyWarningService.class),
                 mock(AlarmWarningRelMapper.class),
                 mock(AlarmWarningRelMapper.class),
                 alarmDataService,
                 alarmDataService,
-                equipmentBaseMapper);
+                equipmentBaseMapper,
+                mock(com.zksy.base.service.EquipmentTypeService.class));
     }
     }
 
 
     /** 注册实体的 lambda 缓存(等价于 MyBatis-Plus 启动时的 TableInfoHelper 初始化)。 */
     /** 注册实体的 lambda 缓存(等价于 MyBatis-Plus 启动时的 TableInfoHelper 初始化)。 */

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

@@ -294,6 +294,67 @@ class CockpitCurrentEventsControllerTest {
         verifyNoInteractions(adapter);
         verifyNoInteractions(adapter);
     }
     }
 
 
+    /**
+     * 票 77:片区在这条端点上真的生效,三条判据成对取正反证 ——
+     * 合法值收窄列表、非法值 400(且不读适配器)、不传=全域(不得把「不传」当成某个片区)。
+     *
+     * <p>回显是双字段口径(与 events/multidimensional-analysis 同一份):未筛时
+     * district/districtName 都为 null;只看未分区时 district=null 但 districtName=「未分区」,
+     * 前端据此区分「没筛」和「筛了个空片区」。
+     */
+    @Test
+    void districtFilterNarrowsListEchoesBothFieldsAndKeepsAbsentAsGlobal() throws Exception {
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("district", "0"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(0))
+                .andExpect(jsonPath("$.data.filters.districtName").value("城南"))
+                .andExpect(jsonPath("$.data.pagination.total").value(1))
+                .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-002"));
+
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("district", "1"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(1))
+                .andExpect(jsonPath("$.data.pagination.total").value(1))
+                .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("W-003"));
+
+        // 只看未分区:没有可信片区的事件如实归入「未分区」,不被静默丢弃
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("district", "UNASSIGNED"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(nullValue()))
+                .andExpect(jsonPath("$.data.filters.districtName").value("未分区"))
+                .andExpect(jsonPath("$.data.pagination.total").value(3));
+
+        // 不传=全域
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(nullValue()))
+                .andExpect(jsonPath("$.data.filters.districtName").value(nullValue()))
+                .andExpect(jsonPath("$.data.pagination.total").value(5));
+
+        // 报警模式同一套语义(片区是事件族共同维度,不是预警专属)
+        mockMvc.perform(get(ENDPOINT).param("mode", "ALARM").param("district", "2"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(2))
+                .andExpect(jsonPath("$.data.filters.districtName").value("太常"))
+                .andExpect(jsonPath("$.data.pagination.total").value(1))
+                .andExpect(jsonPath("$.data.sections.events.data[0].eventNo").value("A-001"));
+    }
+
+    @Test
+    void invalidDistrictReturnsTypedBadRequestBeforeReadAdapterIsCalled() throws Exception {
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("district", "99"))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.code").value(400))
+                .andExpect(jsonPath("$.data.category").value("INVALID_REQUEST"))
+                .andExpect(jsonPath("$.data.message").value("district must be 0, 1, 2 or UNASSIGNED"));
+
+        mockMvc.perform(get(ENDPOINT).param("mode", "WARNING").param("district", "NOT_A_DISTRICT"))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.data.message").value("district must be 0, 1, 2 or UNASSIGNED"));
+
+        verifyNoInteractions(adapter);
+    }
+
     @Test
     @Test
     void serverFailureReturnsTypedServerError() throws Exception {
     void serverFailureReturnsTypedServerError() throws Exception {
         when(adapter.load()).thenThrow(new CockpitReadException("upstream unavailable"));
         when(adapter.load()).thenThrow(new CockpitReadException("upstream unavailable"));
@@ -352,11 +413,11 @@ class CockpitCurrentEventsControllerTest {
                         warning("W-002", "燃气压力预警", "GAS_PRESSURE", 2, "ACTIVE",
                         warning("W-002", "燃气压力预警", "GAS_PRESSURE", 2, "ACTIVE",
                                 "2026-09-02T09:45:00+08:00", "2026-09-02T09:46:00+08:00",
                                 "2026-09-02T09:45:00+08:00", "2026-09-02T09:46:00+08:00",
                                 "0.32", "MPa", "D-GAS-2", "燃气浓度计-002", "MP-A-002", "城区A-燃气2号点", "城区A-燃气2号点",
                                 "0.32", "MPa", "D-GAS-2", "燃气浓度计-002", "MP-A-002", "城区A-燃气2号点", "城区A-燃气2号点",
-                                "GAS_SENSOR", "110.361", "28.441"),
+                                "GAS_SENSOR", "110.361", "28.441", 0),
                         warning("W-003", "供水压力预警", "WATER_PRESSURE", 3, "PENDING",
                         warning("W-003", "供水压力预警", "WATER_PRESSURE", 3, "PENDING",
                                 "2026-09-02T09:40:00+08:00", "2026-09-02T09:41:00+08:00",
                                 "2026-09-02T09:40:00+08:00", "2026-09-02T09:41:00+08:00",
                                 "0.18", "MPa", "D-WS-1", "供水压力计-001", "MP-B-001", "城区B-供水1号点", "城区B-供水1号点",
                                 "0.18", "MPa", "D-WS-1", "供水压力计-001", "MP-B-001", "城区B-供水1号点", "城区B-供水1号点",
-                                "WATER_SUPPLY_SENSOR", "110.380", "28.460"),
+                                "WATER_SUPPLY_SENSOR", "110.380", "28.460", 1),
                         warning("W-004", "污水浓度预警", "SEWAGE_CONCENTRATION", 4, "RELEASED",
                         warning("W-004", "污水浓度预警", "SEWAGE_CONCENTRATION", 4, "RELEASED",
                                 "2026-09-02T09:35:00+08:00", "2026-09-02T09:36:00+08:00",
                                 "2026-09-02T09:35:00+08:00", "2026-09-02T09:36:00+08:00",
                                 "18.2", "mg/L", "D-DR-1", "污水环境监测仪-001", null, null, "城区C-污水1号点",
                                 "18.2", "mg/L", "D-DR-1", "污水环境监测仪-001", null, null, "城区C-污水1号点",
@@ -377,7 +438,7 @@ class CockpitCurrentEventsControllerTest {
                         alarm("A-001", "燃气浓度报警", "GAS_CONCENTRATION", 1, "HANDLED",
                         alarm("A-001", "燃气浓度报警", "GAS_CONCENTRATION", 1, "HANDLED",
                                 "2026-09-02T09:55:00+08:00", "2026-09-02T09:56:00+08:00",
                                 "2026-09-02T09:55:00+08:00", "2026-09-02T09:56:00+08:00",
                                 "22.6", "mg/m³", "D-GAS-3", "燃气浓度计-003", "MP-A-003", "城区A-燃气3号点", "城区A-燃气3号点",
                                 "22.6", "mg/m³", "D-GAS-3", "燃气浓度计-003", "MP-A-003", "城区A-燃气3号点", "城区A-燃气3号点",
-                                "GAS_SENSOR", "110.362", "28.442", true, "0.00", "20.00"),
+                                "GAS_SENSOR", "110.362", "28.442", true, "0.00", "20.00", 2),
                         alarm("A-002", "污水浓度报警", "SEWAGE_CONCENTRATION", 2, "PROCESSING",
                         alarm("A-002", "污水浓度报警", "SEWAGE_CONCENTRATION", 2, "PROCESSING",
                                 "2026-09-02T09:32:00+08:00", "2026-09-02T09:33:00+08:00",
                                 "2026-09-02T09:32:00+08:00", "2026-09-02T09:33:00+08:00",
                                 "42.1", "mg/L", "D-DR-1", "污水环境监测仪-001", "MP-C-001", "城区C-污水1号点", "城区C-污水1号点",
                                 "42.1", "mg/L", "D-DR-1", "污水环境监测仪-001", "MP-C-001", "城区C-污水1号点", "城区C-污水1号点",
@@ -447,11 +508,35 @@ class CockpitCurrentEventsControllerTest {
             String deviceCode, String deviceName, String monitoringPointCode, String monitoringPointName,
             String deviceCode, String deviceName, String monitoringPointCode, String monitoringPointName,
             String location, String typeCode,
             String location, String typeCode,
             String longitude, String latitude, boolean coordinateVerified) {
             String longitude, String latitude, boolean coordinateVerified) {
+        return warning(no, name, type, level, status, eventTime, updatedTime, value, unit,
+                deviceCode, deviceName, monitoringPointCode, monitoringPointName, location, typeCode,
+                longitude, latitude, coordinateVerified, null);
+    }
+
+    /** 票 77:带片区的预警(片区是筛选维度,必须能单独造样本)。 */
+    private static CockpitCurrentEventsSnapshot.SourceWarning warning(
+            String no, String name, String type, Integer level, String status,
+            String eventTime, String updatedTime, String value, String unit,
+            String deviceCode, String deviceName, String monitoringPointCode, String monitoringPointName,
+            String location, String typeCode,
+            String longitude, String latitude, Integer district) {
+        return warning(no, name, type, level, status, eventTime, updatedTime, value, unit,
+                deviceCode, deviceName, monitoringPointCode, monitoringPointName, location, typeCode,
+                longitude, latitude, false, district);
+    }
+
+    /** 最内层:真正构造快照的地方,片区随样本传入(null=未分区)。 */
+    private static CockpitCurrentEventsSnapshot.SourceWarning warning(
+            String no, String name, String type, Integer level, String status,
+            String eventTime, String updatedTime, String value, String unit,
+            String deviceCode, String deviceName, String monitoringPointCode, String monitoringPointName,
+            String location, String typeCode,
+            String longitude, String latitude, boolean coordinateVerified, Integer district) {
         return new CockpitCurrentEventsSnapshot.SourceWarning(
         return new CockpitCurrentEventsSnapshot.SourceWarning(
                 no, name, type, level, status, eventTime, updatedTime,
                 no, name, type, level, status, eventTime, updatedTime,
                 new BigDecimal(value), unit, deviceCode, deviceName, monitoringPointCode, monitoringPointName,
                 new BigDecimal(value), unit, deviceCode, deviceName, monitoringPointCode, monitoringPointName,
                 location, typeCode,
                 location, typeCode,
-                new BigDecimal(longitude), new BigDecimal(latitude), coordinateVerified);
+                new BigDecimal(longitude), new BigDecimal(latitude), coordinateVerified, district);
     }
     }
 
 
     private static CockpitCurrentEventsSnapshot.SourceAlarm alarm(
     private static CockpitCurrentEventsSnapshot.SourceAlarm alarm(
@@ -472,13 +557,28 @@ class CockpitCurrentEventsControllerTest {
             String location, String typeCode,
             String location, String typeCode,
             String longitude, String latitude,
             String longitude, String latitude,
             boolean coordinateVerified, String thresholdMinValue, String thresholdMaxValue) {
             boolean coordinateVerified, String thresholdMinValue, String thresholdMaxValue) {
+        return alarm(id, name, type, level, status, eventTime, updatedTime, value, unit,
+                deviceCode, deviceName, monitoringPointCode, monitoringPointName, location, typeCode,
+                longitude, latitude, coordinateVerified, thresholdMinValue, thresholdMaxValue, null);
+    }
+
+    /** 票 77:带片区的报警(最内层构造,片区随样本传入;null=未分区)。 */
+    private static CockpitCurrentEventsSnapshot.SourceAlarm alarm(
+            String id, String name, String type, Integer level, String status,
+            String eventTime, String updatedTime, String value, String unit,
+            String deviceCode, String deviceName, String monitoringPointCode, String monitoringPointName,
+            String location, String typeCode,
+            String longitude, String latitude,
+            boolean coordinateVerified, String thresholdMinValue, String thresholdMaxValue,
+            Integer district) {
         return new CockpitCurrentEventsSnapshot.SourceAlarm(
         return new CockpitCurrentEventsSnapshot.SourceAlarm(
                 id, name, type, level, status, eventTime, updatedTime,
                 id, name, type, level, status, eventTime, updatedTime,
                 new BigDecimal(value), unit, deviceCode, deviceName, monitoringPointCode, monitoringPointName,
                 new BigDecimal(value), unit, deviceCode, deviceName, monitoringPointCode, monitoringPointName,
                 location, typeCode,
                 location, typeCode,
                 new BigDecimal(longitude), new BigDecimal(latitude), coordinateVerified,
                 new BigDecimal(longitude), new BigDecimal(latitude), coordinateVerified,
                 thresholdMinValue == null ? null : new BigDecimal(thresholdMinValue),
                 thresholdMinValue == null ? null : new BigDecimal(thresholdMinValue),
-                thresholdMaxValue == null ? null : new BigDecimal(thresholdMaxValue));
+                thresholdMaxValue == null ? null : new BigDecimal(thresholdMaxValue),
+                district);
     }
     }
 
 
     private static CockpitCurrentEventsSnapshot.SourceWorkOrderLink workOrder(
     private static CockpitCurrentEventsSnapshot.SourceWorkOrderLink workOrder(

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

@@ -138,6 +138,76 @@ class CockpitHandlingEfficiencyControllerTest {
                 .andExpect(jsonPath("$.data.durationTrend[4].eventCount").value(0));
                 .andExpect(jsonPath("$.data.durationTrend[4].eventCount").value(0));
     }
     }
 
 
+    /**
+     * 票 77:片区在预警效率上真的生效,且**事件口径与工单口径同时收窄** ——
+     * 只筛事件会让同一响应里「事件 1 条 / 处置率却是全域 2/3」,正是票 67 反馈的
+     * 「同一模式四个数各说各话」;所以工单范围(分母)必须按同一片区收敛。
+     */
+    @Test
+    void districtFilterNarrowsEventsAndWorkOrderScopeTogether() throws Exception {
+        when(adapter.load()).thenReturn(fixtureWithDistricts());
+
+        // 报警族:事件 1 条;分母只算城南的 2 张工单(状态 5 与 4),分子 1
+        mockMvc.perform(get(ENDPOINT)
+                        .param("eventFamily", "ALARM")
+                        .param("district", "0")
+                        .param("startTime", START)
+                        .param("endTime", END))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(0))
+                .andExpect(jsonPath("$.data.filters.districtName").value("城南"))
+                .andExpect(jsonPath("$.data.summary.totalEvents").value(1))
+                .andExpect(jsonPath("$.data.summary.handledCount").value(1))
+                .andExpect(jsonPath("$.data.summary.handlingEfficiency.numerator").value(1))
+                .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(2))
+                .andExpect(jsonPath("$.data.workOrderStateCounts[5].count").value(1))
+                .andExpect(jsonPath("$.data.workOrderStateCounts[4].count").value(1))
+                .andExpect(jsonPath("$.data.workOrderStateCounts[0].count").value(0));
+
+        // 预警族:同一套片区语义(预警不挂工单,故只收窄事件口径)
+        mockMvc.perform(get(ENDPOINT)
+                        .param("eventFamily", "WARNING")
+                        .param("district", "2")
+                        .param("startTime", START)
+                        .param("endTime", END))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(2))
+                .andExpect(jsonPath("$.data.filters.districtName").value("太常"))
+                .andExpect(jsonPath("$.data.summary.totalEvents").value(1));
+
+        // 只看未分区:没有可信片区的预警照实保留
+        mockMvc.perform(get(ENDPOINT)
+                        .param("eventFamily", "WARNING")
+                        .param("district", "UNASSIGNED")
+                        .param("startTime", START)
+                        .param("endTime", END))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(nullValue()))
+                .andExpect(jsonPath("$.data.filters.districtName").value("未分区"))
+                .andExpect(jsonPath("$.data.summary.totalEvents").value(1));
+
+        // 不传=全域(双字段皆空,前端据此区分「没筛」与「筛了未分区」)
+        mockMvc.perform(get(ENDPOINT)
+                        .param("eventFamily", "ALARM")
+                        .param("startTime", START)
+                        .param("endTime", END))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(nullValue()))
+                .andExpect(jsonPath("$.data.filters.districtName").value(nullValue()))
+                .andExpect(jsonPath("$.data.summary.totalEvents").value(2))
+                .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(4));
+    }
+
+    @Test
+    void invalidDistrictReturnsTypedBadRequestBeforeReadAdapterIsCalled() throws Exception {
+        mockMvc.perform(get(ENDPOINT).param("eventFamily", "ALARM").param("district", "99"))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.code").value(400))
+                .andExpect(jsonPath("$.data.message").value("district must be 0, 1, 2 or UNASSIGNED"));
+
+        verifyNoInteractions(adapter);
+    }
+
     @Test
     @Test
     void warningFamilyPutsUnrecognisedStatusIntoUnknownBucketSoBucketsStillSumToTotal() throws Exception {
     void warningFamilyPutsUnrecognisedStatusIntoUnknownBucketSoBucketsStillSumToTotal() throws Exception {
         when(adapter.load()).thenReturn(fixtureWithDirtyWarningStatus());
         when(adapter.load()).thenReturn(fixtureWithDirtyWarningStatus());
@@ -556,8 +626,14 @@ class CockpitHandlingEfficiencyControllerTest {
 
 
     private static CockpitHandlingEfficiencySnapshot.SourceWorkOrder scopeOrder(
     private static CockpitHandlingEfficiencySnapshot.SourceWorkOrder scopeOrder(
             String orderNo, Integer stateCode, String typeCode) {
             String orderNo, Integer stateCode, String typeCode) {
+        return scopeOrder(orderNo, stateCode, typeCode, null);
+    }
+
+    /** 票 77:工单范围带片区(处置率分母与事件同口径收窄)。 */
+    private static CockpitHandlingEfficiencySnapshot.SourceWorkOrder scopeOrder(
+            String orderNo, Integer stateCode, String typeCode, Integer district) {
         return new CockpitHandlingEfficiencySnapshot.SourceWorkOrder(
         return new CockpitHandlingEfficiencySnapshot.SourceWorkOrder(
-                orderNo, stateCode, "2026-09-02T09:00:00+08:00", typeCode);
+                orderNo, stateCode, "2026-09-02T09:00:00+08:00", typeCode, district);
     }
     }
 
 
     /** 只含状态 5/6/7 的工单,用于验证分母含 6、排除 7。 */
     /** 只含状态 5/6/7 的工单,用于验证分母含 6、排除 7。 */
@@ -615,6 +691,29 @@ class CockpitHandlingEfficiencyControllerTest {
                         scopeOrder("WO-M3", 5, "GAS_SENSOR")));
                         scopeOrder("WO-M3", 5, "GAS_SENSOR")));
     }
     }
 
 
+    /**
+     * 票 77:片区样本 —— 预警/报警各一条城南、一条太常、一条未分区,
+     * 工单范围同样带片区。用它验证**事件计数与工单口径按同一片区收窄**。
+     */
+    private static CockpitHandlingEfficiencySnapshot fixtureWithDistricts() {
+        return new CockpitHandlingEfficiencySnapshot(
+                "2026-09-02T11:59:50+08:00",
+                types(),
+                Arrays.asList(
+                        warningWithDistrict("EW-S", 1, "2026-09-02T08:10:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001", 0),
+                        warningWithDistrict("EW-T", 1, "2026-09-02T09:10:00+08:00", "GAS_SENSOR", "D-GAS-2", "燃气浓度计-002", 2),
+                        warningWithDistrict("EW-U", 1, "2026-09-02T10:10:00+08:00", "GAS_SENSOR", "D-GAS-3", "燃气浓度计-003", null)),
+                Arrays.asList(
+                        alarm("EA-S", 1, "2026-09-02T08:20:00+08:00", "GAS_SENSOR", "D-GAS-1", "燃气浓度计-001", 0),
+                        alarm("EA-T", 1, "2026-09-02T09:20:00+08:00", "GAS_SENSOR", "D-GAS-2", "燃气浓度计-002", 2)),
+                Collections.emptyList(),
+                Arrays.asList(
+                        scopeOrder("WO-S5", 5, "GAS_SENSOR", 0),
+                        scopeOrder("WO-S4", 4, "GAS_SENSOR", 0),
+                        scopeOrder("WO-T", 5, "GAS_SENSOR", 2),
+                        scopeOrder("WO-U", 0, "GAS_SENSOR", null)));
+    }
+
     private static java.util.List<CockpitHandlingEfficiencySnapshot.SourceType> types() {
     private static java.util.List<CockpitHandlingEfficiencySnapshot.SourceType> types() {
         return Arrays.asList(
         return Arrays.asList(
                 new CockpitHandlingEfficiencySnapshot.SourceType(
                 new CockpitHandlingEfficiencySnapshot.SourceType(
@@ -627,21 +726,35 @@ class CockpitHandlingEfficiencyControllerTest {
 
 
     private static CockpitHandlingEfficiencySnapshot.SourceWarning warning(
     private static CockpitHandlingEfficiencySnapshot.SourceWarning warning(
             String no, Integer level, String eventTime, String typeCode, String deviceCode, String deviceName) {
             String no, Integer level, String eventTime, String typeCode, String deviceCode, String deviceName) {
+        return warningWithWorkflow(no, level, eventTime, typeCode, deviceCode, deviceName, null, null);
+    }
+
+    /** 票 77:带片区的预警样本(生命周期留空,只关心片区)。 */
+    private static CockpitHandlingEfficiencySnapshot.SourceWarning warningWithDistrict(
+            String no, Integer level, String eventTime, String typeCode, String deviceCode,
+            String deviceName, Integer district) {
         return new CockpitHandlingEfficiencySnapshot.SourceWarning(
         return new CockpitHandlingEfficiencySnapshot.SourceWarning(
-                no, level, eventTime, typeCode, deviceCode, deviceName);
+                no, level, eventTime, typeCode, deviceCode, deviceName, null, null, district);
     }
     }
 
 
     private static CockpitHandlingEfficiencySnapshot.SourceWarning warningWithWorkflow(
     private static CockpitHandlingEfficiencySnapshot.SourceWarning warningWithWorkflow(
             String no, Integer level, String eventTime, String typeCode, String deviceCode, String deviceName,
             String no, Integer level, String eventTime, String typeCode, String deviceCode, String deviceName,
             String status, String updateTime) {
             String status, String updateTime) {
         return new CockpitHandlingEfficiencySnapshot.SourceWarning(
         return new CockpitHandlingEfficiencySnapshot.SourceWarning(
-                no, level, eventTime, typeCode, deviceCode, deviceName, status, updateTime);
+                no, level, eventTime, typeCode, deviceCode, deviceName, status, updateTime, null);
     }
     }
 
 
     private static CockpitHandlingEfficiencySnapshot.SourceAlarm alarm(
     private static CockpitHandlingEfficiencySnapshot.SourceAlarm alarm(
             String id, Integer level, String eventTime, String typeCode, String deviceCode, String deviceName) {
             String id, Integer level, String eventTime, String typeCode, String deviceCode, String deviceName) {
+        return alarm(id, level, eventTime, typeCode, deviceCode, deviceName, null);
+    }
+
+    /** 票 77:带片区的报警样本。 */
+    private static CockpitHandlingEfficiencySnapshot.SourceAlarm alarm(
+            String id, Integer level, String eventTime, String typeCode, String deviceCode,
+            String deviceName, Integer district) {
         return new CockpitHandlingEfficiencySnapshot.SourceAlarm(
         return new CockpitHandlingEfficiencySnapshot.SourceAlarm(
-                id, level, eventTime, typeCode, deviceCode, deviceName);
+                id, level, eventTime, typeCode, deviceCode, deviceName, district);
     }
     }
 
 
     private static CockpitHandlingEfficiencySnapshot.SourceWorkOrderLink link(
     private static CockpitHandlingEfficiencySnapshot.SourceWorkOrderLink link(

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

@@ -112,6 +112,72 @@ class CockpitHistoryAnalysisControllerTest {
                 .andExpect(jsonPath("$.data.rows[3].handlingEfficiency.value").value("0.0000"));
                 .andExpect(jsonPath("$.data.rows[3].handlingEfficiency.value").value("0.0000"));
     }
     }
 
 
+    /**
+     * 票 77:片区在历史分析上真的生效 —— 事件行与汇总处置率的工单范围**同一片区口径**。
+     *
+     * <p>只筛事件、不筛工单会造出「事件按城南 1 条、处置率却是全域 2/5」这种同一响应内
+     * 两个口径打架的读数(正是票 67 反馈的「四个数各说各话」),所以分母必须一起收窄。
+     */
+    @Test
+    void districtFilterNarrowsEventsAndScopeRateTogether() throws Exception {
+        mockMvc.perform(get(ENDPOINT)
+                        .param("eventFamily", "WARNING")
+                        .param("district", "0")
+                        .param("startTime", START)
+                        .param("endTime", END))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(0))
+                .andExpect(jsonPath("$.data.filters.districtName").value("城南"))
+                .andExpect(jsonPath("$.data.summary.eventCount").value(1))
+                .andExpect(jsonPath("$.data.events", hasSize(1)))
+                .andExpect(jsonPath("$.data.events[0].eventNo").value("HW-002"))
+                .andExpect(jsonPath("$.data.summary.handlingEfficiency.numerator").value(1))
+                .andExpect(jsonPath("$.data.summary.handlingEfficiency.denominator").value(2));
+
+        // 报警族同一套语义
+        mockMvc.perform(get(ENDPOINT)
+                        .param("eventFamily", "ALARM")
+                        .param("district", "2")
+                        .param("startTime", START)
+                        .param("endTime", END))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(2))
+                .andExpect(jsonPath("$.data.filters.districtName").value("太常"))
+                .andExpect(jsonPath("$.data.summary.eventCount").value(1))
+                .andExpect(jsonPath("$.data.events[0].eventNo").value("HA-001"));
+
+        // 只看未分区:没有可信片区的事件照实保留,不被静默丢弃
+        mockMvc.perform(get(ENDPOINT)
+                        .param("eventFamily", "WARNING")
+                        .param("district", "UNASSIGNED")
+                        .param("startTime", START)
+                        .param("endTime", END))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(nullValue()))
+                .andExpect(jsonPath("$.data.filters.districtName").value("未分区"))
+                .andExpect(jsonPath("$.data.summary.eventCount").value(3));
+
+        // 不传=全域(双字段都为空,不是「未分区」)
+        mockMvc.perform(get(ENDPOINT)
+                        .param("eventFamily", "WARNING")
+                        .param("startTime", START)
+                        .param("endTime", END))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.filters.district").value(nullValue()))
+                .andExpect(jsonPath("$.data.filters.districtName").value(nullValue()))
+                .andExpect(jsonPath("$.data.summary.eventCount").value(5));
+    }
+
+    @Test
+    void invalidDistrictReturnsTypedBadRequestBeforeReadAdapterIsCalled() throws Exception {
+        mockMvc.perform(get(ENDPOINT).param("eventFamily", "WARNING").param("district", "99"))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.code").value(400))
+                .andExpect(jsonPath("$.data.message").value("district must be 0, 1, 2 or UNASSIGNED"));
+
+        verifyNoInteractions(adapter);
+    }
+
     @Test
     @Test
     void alarmFamilyIsIndependentFromWarnings() throws Exception {
     void alarmFamilyIsIndependentFromWarnings() throws Exception {
         mockMvc.perform(get(ENDPOINT)
         mockMvc.perform(get(ENDPOINT)
@@ -385,7 +451,7 @@ class CockpitHistoryAnalysisControllerTest {
         return new CockpitHistoryAnalysisService(
         return new CockpitHistoryAnalysisService(
                 adapter,
                 adapter,
                 Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC))
                 Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC))
-                .load("direct", family, null, null, start, end, 1, 100);
+                .load("direct", family, null, null, null, start, end, 1, 100);
     }
     }
 
 
     private static CockpitHistoryAnalysisSnapshot fixture() {
     private static CockpitHistoryAnalysisSnapshot fixture() {
@@ -401,14 +467,14 @@ class CockpitHistoryAnalysisControllerTest {
                                 "SMART_MANHOLE", "智能井盖", null, "SMART_DEVICE")),
                                 "SMART_MANHOLE", "智能井盖", null, "SMART_DEVICE")),
                 Arrays.asList(
                 Arrays.asList(
                         warning("HW-001", 1, "2026-09-01T23:30:00+08:00", "GAS_SENSOR", "D-GAS-1"),
                         warning("HW-001", 1, "2026-09-01T23:30:00+08:00", "GAS_SENSOR", "D-GAS-1"),
-                        warning("HW-002", 2, "2026-09-02T00:15:00+08:00", "GAS_SENSOR", "D-GAS-2"),
-                        warning("HW-003", 2, "2026-09-02T00:45:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-1"),
+                        warning("HW-002", 2, "2026-09-02T00:15:00+08:00", "GAS_SENSOR", "D-GAS-2", 0),
+                        warning("HW-003", 2, "2026-09-02T00:45:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-1", 1),
                         warning("HW-004", 4, "2026-09-02T01:20:00+08:00", "SEWAGE_ENV", "D-DR-1"),
                         warning("HW-004", 4, "2026-09-02T01:20:00+08:00", "SEWAGE_ENV", "D-DR-1"),
                         warning("HW-005", 1, "2026-09-02T01:50:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-2"),
                         warning("HW-005", 1, "2026-09-02T01:50:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-2"),
                         warning("HW-LEGACY", 1, "2026-09-02T00:10:00+08:00", "LEGACY", "D-LEGACY"),
                         warning("HW-LEGACY", 1, "2026-09-02T00:10:00+08:00", "LEGACY", "D-LEGACY"),
                         warning("HW-MH", 1, "2026-09-01T23:59:00+08:00", "SMART_MANHOLE", "D-MH")),
                         warning("HW-MH", 1, "2026-09-01T23:59:00+08:00", "SMART_MANHOLE", "D-MH")),
                 Arrays.asList(
                 Arrays.asList(
-                        alarm("HA-001", 3, "2026-09-02T01:10:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-3"),
+                        alarm("HA-001", 3, "2026-09-02T01:10:00+08:00", "WATER_SUPPLY_SENSOR", "D-WS-3", 2),
                         alarm("HA-002", 1, "2026-09-02T01:30:00+08:00", "GAS_SENSOR", "D-GAS-3")),
                         alarm("HA-002", 1, "2026-09-02T01:30:00+08:00", "GAS_SENSOR", "D-GAS-3")),
                 Arrays.asList(
                 Arrays.asList(
                         workOrder("HW-001", 5),
                         workOrder("HW-001", 5),
@@ -420,17 +486,23 @@ class CockpitHistoryAnalysisControllerTest {
                 // 汇总处置率的工单范围(分母来源):状态 5/5/4/1/0 → 分母 5、分子 2(状态 5)
                 // 汇总处置率的工单范围(分母来源):状态 5/5/4/1/0 → 分母 5、分子 2(状态 5)
                 // → 0.4000;状态 7 与关联关系无关,故不受 workOrders 影响。
                 // → 0.4000;状态 7 与关联关系无关,故不受 workOrders 影响。
                 Arrays.asList(
                 Arrays.asList(
-                        scopeOrder(5, "GAS_SENSOR"),
-                        scopeOrder(5, "WATER_SUPPLY_SENSOR"),
-                        scopeOrder(4, "SEWAGE_ENV"),
-                        scopeOrder(1, "GAS_SENSOR"),
-                        scopeOrder(0, "GAS_SENSOR")));
+                        scopeOrder(5, "GAS_SENSOR", 0),
+                        scopeOrder(5, "WATER_SUPPLY_SENSOR", 1),
+                        scopeOrder(4, "SEWAGE_ENV", 0),
+                        scopeOrder(1, "GAS_SENSOR", null),
+                        scopeOrder(0, "GAS_SENSOR", 2)));
     }
     }
 
 
     private static CockpitHistoryAnalysisSnapshot.SourceWorkOrder scopeOrder(
     private static CockpitHistoryAnalysisSnapshot.SourceWorkOrder scopeOrder(
             Integer stateCode, String typeCode) {
             Integer stateCode, String typeCode) {
+        return scopeOrder(stateCode, typeCode, null);
+    }
+
+    /** 票 77:工单范围也带片区(处置率的分母与事件同口径收窄)。 */
+    private static CockpitHistoryAnalysisSnapshot.SourceWorkOrder scopeOrder(
+            Integer stateCode, String typeCode, Integer district) {
         return new CockpitHistoryAnalysisSnapshot.SourceWorkOrder(
         return new CockpitHistoryAnalysisSnapshot.SourceWorkOrder(
-                stateCode, "2026-09-02T01:00:00+08:00", typeCode);
+                stateCode, "2026-09-02T01:00:00+08:00", typeCode, district);
     }
     }
 
 
     private static CockpitHistoryAnalysisSnapshot.SourceType type(String code, String industry) {
     private static CockpitHistoryAnalysisSnapshot.SourceType type(String code, String industry) {
@@ -440,14 +512,29 @@ class CockpitHistoryAnalysisControllerTest {
 
 
     private static CockpitHistoryAnalysisSnapshot.SourceWarning warning(
     private static CockpitHistoryAnalysisSnapshot.SourceWarning warning(
             String no, Integer level, String eventTime, String typeCode, String deviceCode) {
             String no, Integer level, String eventTime, String typeCode, String deviceCode) {
+        return warning(no, level, eventTime, typeCode, deviceCode, null);
+    }
+
+    /** 票 77:带片区的预警样本。 */
+    private static CockpitHistoryAnalysisSnapshot.SourceWarning warning(
+            String no, Integer level, String eventTime, String typeCode, String deviceCode,
+            Integer district) {
         return new CockpitHistoryAnalysisSnapshot.SourceWarning(
         return new CockpitHistoryAnalysisSnapshot.SourceWarning(
-                no, level, eventTime, typeCode, deviceCode);
+                no, level, eventTime, typeCode, deviceCode, null, null, null, null, district);
     }
     }
 
 
     private static CockpitHistoryAnalysisSnapshot.SourceAlarm alarm(
     private static CockpitHistoryAnalysisSnapshot.SourceAlarm alarm(
             String id, Integer level, String eventTime, String typeCode, String deviceCode) {
             String id, Integer level, String eventTime, String typeCode, String deviceCode) {
+        return alarm(id, level, eventTime, typeCode, deviceCode, null);
+    }
+
+    /** 票 77:带片区的报警样本。 */
+    private static CockpitHistoryAnalysisSnapshot.SourceAlarm alarm(
+            String id, Integer level, String eventTime, String typeCode, String deviceCode,
+            Integer district) {
         return new CockpitHistoryAnalysisSnapshot.SourceAlarm(
         return new CockpitHistoryAnalysisSnapshot.SourceAlarm(
-                id, level, eventTime, typeCode, deviceCode);
+                id, level, eventTime, typeCode, deviceCode, null, null, null, null, null,
+                null, null, null, null, district);
     }
     }
 
 
     private static CockpitHistoryAnalysisSnapshot.SourceWorkOrderLink workOrder(
     private static CockpitHistoryAnalysisSnapshot.SourceWorkOrderLink workOrder(

+ 1 - 1
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitWorkOrderAssociationContractTest.java

@@ -96,7 +96,7 @@ class CockpitWorkOrderAssociationContractTest {
 
 
         BusinessCockpitWorkOrderReadAdapter adapter = new BusinessCockpitWorkOrderReadAdapter(
         BusinessCockpitWorkOrderReadAdapter adapter = new BusinessCockpitWorkOrderReadAdapter(
                 workOrderService, mock(EarlyWarningService.class), mock(AlarmWarningRelMapper.class),
                 workOrderService, mock(EarlyWarningService.class), mock(AlarmWarningRelMapper.class),
-                alarmDataService, equipmentMapper);
+                alarmDataService, equipmentMapper, mock(com.zksy.base.service.EquipmentTypeService.class));
         CockpitWorkOrderReadService service = new CockpitWorkOrderReadService(
         CockpitWorkOrderReadService service = new CockpitWorkOrderReadService(
                 adapter, Clock.fixed(Instant.parse("2026-10-10T07:11:30Z"), ZoneOffset.UTC));
                 adapter, Clock.fixed(Instant.parse("2026-10-10T07:11:30Z"), ZoneOffset.UTC));
         mockMvc = MockMvcBuilders.standaloneSetup(new CockpitWorkOrderReadController(service)).build();
         mockMvc = MockMvcBuilders.standaloneSetup(new CockpitWorkOrderReadController(service)).build();

+ 57 - 1
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/CockpitWorkOrderListContractTest.java

@@ -5,7 +5,9 @@ import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.alarm.service.AlarmDataService;
 import com.zksy.base.alarm.service.AlarmDataService;
 import com.zksy.base.domain.EquipmentBase;
 import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentType;
 import com.zksy.base.mapper.EquipmentBaseMapper;
 import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.service.EquipmentTypeService;
 import com.zksy.base.warning.mapper.AlarmWarningRelMapper;
 import com.zksy.base.warning.mapper.AlarmWarningRelMapper;
 import com.zksy.base.warning.service.EarlyWarningService;
 import com.zksy.base.warning.service.EarlyWarningService;
 import com.zksy.cockpit.workorder.BusinessCockpitWorkOrderReadAdapter;
 import com.zksy.cockpit.workorder.BusinessCockpitWorkOrderReadAdapter;
@@ -96,9 +98,14 @@ class CockpitWorkOrderListContractTest {
         when(alarmDataService.list(any(LambdaQueryWrapper.class)))
         when(alarmDataService.list(any(LambdaQueryWrapper.class)))
                 .thenReturn(Collections.singletonList(alarm));
                 .thenReturn(Collections.singletonList(alarm));
 
 
+        EquipmentTypeService equipmentTypeService = mock(EquipmentTypeService.class);
+        // 类型树:1 供水、2 排水、3 燃气三个根 + 一个挂在燃气根下的叶子 11。
+        when(equipmentTypeService.list()).thenReturn(Arrays.asList(
+                equipmentType(1, 0), equipmentType(2, 0), equipmentType(3, 0),
+                equipmentType(11, 3)));
         BusinessCockpitWorkOrderReadAdapter adapter = new BusinessCockpitWorkOrderReadAdapter(
         BusinessCockpitWorkOrderReadAdapter adapter = new BusinessCockpitWorkOrderReadAdapter(
                 workOrderService, mock(EarlyWarningService.class), mock(AlarmWarningRelMapper.class),
                 workOrderService, mock(EarlyWarningService.class), mock(AlarmWarningRelMapper.class),
-                alarmDataService, equipmentMapper);
+                alarmDataService, equipmentMapper, equipmentTypeService);
         CockpitWorkOrderReadService service = new CockpitWorkOrderReadService(
         CockpitWorkOrderReadService service = new CockpitWorkOrderReadService(
                 adapter, Clock.fixed(Instant.parse("2026-10-10T07:11:30Z"), ZoneOffset.UTC));
                 adapter, Clock.fixed(Instant.parse("2026-10-10T07:11:30Z"), ZoneOffset.UTC));
         mockMvc = MockMvcBuilders.standaloneSetup(new CockpitWorkOrderReadController(service)).build();
         mockMvc = MockMvcBuilders.standaloneSetup(new CockpitWorkOrderReadController(service)).build();
@@ -131,6 +138,55 @@ class CockpitWorkOrderListContractTest {
                 .list(any(LambdaQueryWrapper.class));
                 .list(any(LambdaQueryWrapper.class));
     }
     }
 
 
+    /**
+     * 票 77:行业筛选必须真的下到 SQL,并且是**同一套**「工单设备 → 设备类型 → 行业」派生
+     * (与上方状态分布同源,否则会出现分布按行业收敛、列表却是全库的自相矛盾)。
+     * 判据:有效的行业会带出设备子查询;不传则一条设备条件都不加。
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    void industryFilterIsAppliedThroughDeviceTypesAndAbsentMeansGlobal() throws Exception {
+        mockMvc.perform(get(BASE + "/work-orders").param("industry", "GAS"))
+                .andExpect(status().isOk());
+
+        ArgumentCaptor<LambdaQueryWrapper<WorkOrder>> scoped = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
+        verify(workOrderService, org.mockito.Mockito.atLeastOnce()).count(scoped.capture());
+        String scopedSql = scoped.getValue().getSqlSegment();
+        assertTrue(scopedSql.contains("deviceId"), "行业筛选必须落在 device_id 上,实际:" + scopedSql);
+        assertTrue(scopedSql.contains("equipment_base"), "行业必须经设备表派生,实际:" + scopedSql);
+        // 燃气根 3 + 挂在它下面的叶子 11:叶子也要命中,不能只看直接父级。
+        assertTrue(scopedSql.contains("equipment_type_id IN (3, 11)"),
+                "燃气应命中根 3 与其叶子 11,实际:" + scopedSql);
+
+        // 不传行业=全域:不得出现设备子查询
+        ArgumentCaptor<LambdaQueryWrapper<WorkOrder>> clear;
+        org.mockito.Mockito.clearInvocations(workOrderService);
+        mockMvc.perform(get(BASE + "/work-orders")).andExpect(status().isOk());
+        clear = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
+        verify(workOrderService, org.mockito.Mockito.atLeastOnce()).count(clear.capture());
+        String globalSql = clear.getValue().getSqlSegment();
+        assertFalse(globalSql.contains("equipment_base"),
+                "没传行业却加了设备条件:" + globalSql);
+    }
+
+    /** 非法行业 → 400(静默忽略会造出「以为按行业筛了其实是全量」的假绿)。 */
+    @Test
+    void invalidIndustryIsRejected() throws Exception {
+        mockMvc.perform(get(BASE + "/work-orders").param("industry", "WHATEVER"))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.code").value(400))
+                .andExpect(jsonPath("$.data.message").value(
+                        "industry must be GAS, WATER_SUPPLY, DRAINAGE or MANHOLE"));
+    }
+
+    private static EquipmentType equipmentType(Integer id, Integer parentId) {
+        EquipmentType type = new EquipmentType();
+        type.setId(id);
+        type.setParentId(parentId);
+        type.setTypeName("type-" + id);
+        return type;
+    }
+
     @Test
     @Test
     void invalidStateCodeFallsBackToNoFilter() throws Exception {
     void invalidStateCodeFallsBackToNoFilter() throws Exception {
         mockMvc.perform(get(BASE + "/work-orders").param("stateCode", "99"))
         mockMvc.perform(get(BASE + "/work-orders").param("stateCode", "99"))

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

@@ -124,7 +124,8 @@ public class BusinessCockpitEventAdapter {
             result.add(new CockpitHistoryAnalysisSnapshot.SourceWorkOrder(
             result.add(new CockpitHistoryAnalysisSnapshot.SourceWorkOrder(
                     order.getOrderStatus(),
                     order.getOrderStatus(),
                     format(order.getCreateTime()),
                     format(order.getCreateTime()),
-                    typeCodeOfDeviceId(order.getDeviceId(), facts)));
+                    typeCodeOfDeviceId(order.getDeviceId(), facts),
+                    districtCodeOfDeviceId(order.getDeviceId(), facts)));
         }
         }
         return result;
         return result;
     }
     }
@@ -332,7 +333,8 @@ public class BusinessCockpitEventAdapter {
                 typeCode(warning.getDeviceCode(), facts),
                 typeCode(warning.getDeviceCode(), facts),
                 deviceLongitude(warning.getDeviceCode(), facts),
                 deviceLongitude(warning.getDeviceCode(), facts),
                 deviceLatitude(warning.getDeviceCode(), facts),
                 deviceLatitude(warning.getDeviceCode(), facts),
-                false);
+                false,
+                districtCode(warning.getDeviceCode(), facts));
     }
     }
 
 
     private BigDecimal deviceLongitude(String code, Facts facts) {
     private BigDecimal deviceLongitude(String code, Facts facts) {
@@ -370,7 +372,8 @@ public class BusinessCockpitEventAdapter {
                 coordinate(alarm.getLatitude(), device == null ? null : device.getLatitude()),
                 coordinate(alarm.getLatitude(), device == null ? null : device.getLatitude()),
                 false,
                 false,
                 alarm.getMinValue(),
                 alarm.getMinValue(),
-                alarm.getMaxValue());
+                alarm.getMaxValue(),
+                districtCode(alarm.getDeviceCode(), facts));
     }
     }
 
 
     private List<CockpitCurrentEventsSnapshot.SourceWorkOrderLink> currentWorkOrders(Facts facts) {
     private List<CockpitCurrentEventsSnapshot.SourceWorkOrderLink> currentWorkOrders(Facts facts) {
@@ -424,7 +427,8 @@ public class BusinessCockpitEventAdapter {
                     trimToNull(warning.getStatus()),
                     trimToNull(warning.getStatus()),
                     firstNonBlank(warning.getDeviceName(), deviceName(warning.getDeviceCode(), facts)),
                     firstNonBlank(warning.getDeviceName(), deviceName(warning.getDeviceCode(), facts)),
                     firstNonBlank(warning.getLocation(), point == null ? null : point.location),
                     firstNonBlank(warning.getLocation(), point == null ? null : point.location),
-                    offsetTime(warning.getUpdateTime())));
+                    offsetTime(warning.getUpdateTime()),
+                    districtCode(warning.getDeviceCode(), facts)));
         }
         }
         return result;
         return result;
     }
     }
@@ -450,7 +454,8 @@ public class BusinessCockpitEventAdapter {
                     alarm.getActualValue(),
                     alarm.getActualValue(),
                     warningTypeUnit(metadata),
                     warningTypeUnit(metadata),
                     alarm.getMinValue(),
                     alarm.getMinValue(),
-                    alarm.getMaxValue()));
+                    alarm.getMaxValue(),
+                    districtCode(alarm.getDeviceCode(), facts)));
         }
         }
         return result;
         return result;
     }
     }
@@ -478,7 +483,8 @@ public class BusinessCockpitEventAdapter {
                         // 票 74:预警自身生命周期与解除时刻来源(update_time)随快照下传,
                         // 票 74:预警自身生命周期与解除时刻来源(update_time)随快照下传,
                         // 供服务层按窗口分族聚合;工单口径与这两者无关。
                         // 供服务层按窗口分族聚合;工单口径与这两者无关。
                         trimToNull(item.warning.getStatus()),
                         trimToNull(item.warning.getStatus()),
-                        offsetTime(item.warning.getUpdateTime())))
+                        offsetTime(item.warning.getUpdateTime()),
+                        districtCode(item.warning.getDeviceCode(), facts)))
                 .collect(Collectors.toList());
                 .collect(Collectors.toList());
     }
     }
 
 
@@ -490,7 +496,8 @@ public class BusinessCockpitEventAdapter {
                         offsetTime(item.alarm.getCreateTime()),
                         offsetTime(item.alarm.getCreateTime()),
                         typeCode(item.alarm.getDeviceCode(), facts),
                         typeCode(item.alarm.getDeviceCode(), facts),
                         trimToNull(item.alarm.getDeviceCode()),
                         trimToNull(item.alarm.getDeviceCode()),
-                        firstNonBlank(item.alarm.getDeviceName(), deviceName(item.alarm.getDeviceCode(), facts))))
+                        firstNonBlank(item.alarm.getDeviceName(), deviceName(item.alarm.getDeviceCode(), facts)),
+                        districtCode(item.alarm.getDeviceCode(), facts)))
                 .collect(Collectors.toList());
                 .collect(Collectors.toList());
     }
     }
 
 
@@ -521,11 +528,21 @@ public class BusinessCockpitEventAdapter {
                     trimToNull(order.getOrderNo()),
                     trimToNull(order.getOrderNo()),
                     order.getOrderStatus(),
                     order.getOrderStatus(),
                     format(order.getCreateTime()),
                     format(order.getCreateTime()),
-                    typeCodeOfDeviceId(order.getDeviceId(), facts)));
+                    typeCodeOfDeviceId(order.getDeviceId(), facts),
+                    districtCodeOfDeviceId(order.getDeviceId(), facts)));
         }
         }
         return result;
         return result;
     }
     }
 
 
+    /** 工单设备 → 片区编码(与 {@link #districtCode} 同一权威来源);设备缺失或未识别回 null。 */
+    private Integer districtCodeOfDeviceId(String deviceId, Facts facts) {
+        if (isBlank(deviceId) || facts.devicesById == null) {
+            return null;
+        }
+        EquipmentBase device = facts.devicesById.get(deviceId.trim());
+        return device == null ? null : CockpitDistrict.codeOf(device.getDistrict());
+    }
+
     /** 工单设备 → 设备类型编码;设备缺失或类型未知时回 null。 */
     /** 工单设备 → 设备类型编码;设备缺失或类型未知时回 null。 */
     private String typeCodeOfDeviceId(String deviceId, Facts facts) {
     private String typeCodeOfDeviceId(String deviceId, Facts facts) {
         if (isBlank(deviceId) || facts.devicesById == null) {
         if (isBlank(deviceId) || facts.devicesById == null) {

+ 10 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitCurrentEventsResponse.java

@@ -71,18 +71,27 @@ public class CockpitCurrentEventsResponse {
         private final Integer level;
         private final Integer level;
         private final String industry;
         private final String industry;
         private final String status;
         private final String status;
+        private final Integer district;
+        private final String districtName;
 
 
-        public Filters(String mode, Integer level, String industry, String status) {
+        public Filters(String mode, Integer level, String industry, String status,
+                Integer district, String districtName) {
             this.mode = mode;
             this.mode = mode;
             this.level = level;
             this.level = level;
             this.industry = industry;
             this.industry = industry;
             this.status = status;
             this.status = status;
+            this.district = district;
+            this.districtName = districtName;
         }
         }
 
 
         public String getMode() { return mode; }
         public String getMode() { return mode; }
         public Integer getLevel() { return level; }
         public Integer getLevel() { return level; }
         public String getIndustry() { return industry; }
         public String getIndustry() { return industry; }
         public String getStatus() { return status; }
         public String getStatus() { return status; }
+        /** 生效片区编码(0-城南 1-城北 2-太常);未筛为 {@code null}。 */
+        public Integer getDistrict() { return district; }
+        /** 生效片区名:未筛为 {@code null},只看未分区时为「未分区」;双字段口径见 {@code CockpitDistrict}。 */
+        public String getDistrictName() { return districtName; }
     }
     }
 
 
     public static class Pagination {
     public static class Pagination {

+ 26 - 5
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitCurrentEventsService.java

@@ -2,6 +2,7 @@ package com.zksy.cockpit.event;
 
 
 import com.zksy.cockpit.detail.CockpitSectionResult;
 import com.zksy.cockpit.detail.CockpitSectionResult;
 import com.zksy.cockpit.foundation.CockpitCoordinateValidator;
 import com.zksy.cockpit.foundation.CockpitCoordinateValidator;
+import com.zksy.cockpit.foundation.CockpitDistrict;
 import com.zksy.cockpit.foundation.CockpitErrorCategory;
 import com.zksy.cockpit.foundation.CockpitErrorCategory;
 import com.zksy.cockpit.foundation.CockpitErrorPayload;
 import com.zksy.cockpit.foundation.CockpitErrorPayload;
 import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
 import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
@@ -58,12 +59,19 @@ public class CockpitCurrentEventsService {
             String level,
             String level,
             String industry,
             String industry,
             String status,
             String status,
+            String district,
             Integer page,
             Integer page,
             Integer pageSize) {
             Integer pageSize) {
         String resolvedMode = resolveMode(mode);
         String resolvedMode = resolveMode(mode);
         Integer resolvedLevel = resolveLevel(level);
         Integer resolvedLevel = resolveLevel(level);
         String selectedIndustry = resolveIndustry(industry);
         String selectedIndustry = resolveIndustry(industry);
         String selectedStatus = isBlank(status) ? null : status.trim();
         String selectedStatus = isBlank(status) ? null : status.trim();
+        // 范围维度=片区(票 77):0/1/2 按片区筛、UNASSIGNED 只看未分区、不传=不筛;
+        // 其它值一律 400,报文与 events/multidimensional-analysis 同一份(脏值不得参与筛选)。
+        CockpitDistrict.Query selectedDistrict = CockpitDistrict.parseQuery(district);
+        if (selectedDistrict.invalid()) {
+            throw new CockpitInvalidRequestException("district must be 0, 1, 2 or UNASSIGNED");
+        }
         int safePage = page == null ? 1 : page;
         int safePage = page == null ? 1 : page;
         int safePageSize = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
         int safePageSize = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
         validatePagination(safePage, safePageSize);
         validatePagination(safePage, safePageSize);
@@ -74,6 +82,7 @@ public class CockpitCurrentEventsService {
                 resolvedLevel,
                 resolvedLevel,
                 selectedIndustry,
                 selectedIndustry,
                 selectedStatus,
                 selectedStatus,
+                selectedDistrict,
                 safePage,
                 safePage,
                 safePageSize);
                 safePageSize);
     }
     }
@@ -110,13 +119,14 @@ public class CockpitCurrentEventsService {
             Integer level,
             Integer level,
             String industry,
             String industry,
             String status,
             String status,
+            CockpitDistrict.Query district,
             int page,
             int page,
             int pageSize) {
             int pageSize) {
         CockpitCurrentEventsSnapshot snapshot = state.snapshot;
         CockpitCurrentEventsSnapshot snapshot = state.snapshot;
         Map<String, CockpitCurrentEventsSnapshot.SourceType> types = types(snapshot);
         Map<String, CockpitCurrentEventsSnapshot.SourceType> types = types(snapshot);
         List<EventRow> rows = WARNING.equals(mode)
         List<EventRow> rows = WARNING.equals(mode)
-                ? warningRows(snapshot, types, level, industry, status)
-                : alarmRows(snapshot, types, level, industry, status);
+                ? warningRows(snapshot, types, level, industry, status, district)
+                : alarmRows(snapshot, types, level, industry, status, district);
         rows.sort(eventOrdering());
         rows.sort(eventOrdering());
 
 
         int total = rows.size();
         int total = rows.size();
@@ -156,7 +166,10 @@ public class CockpitCurrentEventsService {
                 "Asia/Shanghai",
                 "Asia/Shanghai",
                 mode,
                 mode,
                 updatedAt,
                 updatedAt,
-                new CockpitCurrentEventsResponse.Filters(mode, level, industry, status),
+                new CockpitCurrentEventsResponse.Filters(
+                        mode, level, industry, status,
+                        district.code(),
+                        district.active() ? CockpitDistrict.nameOf(district.code()) : null),
                 new CockpitCurrentEventsResponse.Pagination(
                 new CockpitCurrentEventsResponse.Pagination(
                         page, pageSize, total, totalPages, toIndex < total),
                         page, pageSize, total, totalPages, toIndex < total),
                 new CockpitCurrentEventsResponse.MapFilter(
                 new CockpitCurrentEventsResponse.MapFilter(
@@ -169,7 +182,8 @@ public class CockpitCurrentEventsService {
             Map<String, CockpitCurrentEventsSnapshot.SourceType> types,
             Map<String, CockpitCurrentEventsSnapshot.SourceType> types,
             Integer level,
             Integer level,
             String industry,
             String industry,
-            String status) {
+            String status,
+            CockpitDistrict.Query district) {
         List<EventRow> rows = new ArrayList<>();
         List<EventRow> rows = new ArrayList<>();
         if (snapshot.getWarnings() == null) {
         if (snapshot.getWarnings() == null) {
             return rows;
             return rows;
@@ -188,6 +202,9 @@ public class CockpitCurrentEventsService {
             if (status != null && !status.equals(warning.getStatus())) {
             if (status != null && !status.equals(warning.getStatus())) {
                 continue;
                 continue;
             }
             }
+            if (!CockpitDistrict.matches(district, warning.getDistrict())) {
+                continue;
+            }
             rows.add(new EventRow(
             rows.add(new EventRow(
                     warning.getWarningNo(),
                     warning.getWarningNo(),
                     warning.getWarningName(),
                     warning.getWarningName(),
@@ -218,7 +235,8 @@ public class CockpitCurrentEventsService {
             Map<String, CockpitCurrentEventsSnapshot.SourceType> types,
             Map<String, CockpitCurrentEventsSnapshot.SourceType> types,
             Integer level,
             Integer level,
             String industry,
             String industry,
-            String status) {
+            String status,
+            CockpitDistrict.Query district) {
         List<EventRow> rows = new ArrayList<>();
         List<EventRow> rows = new ArrayList<>();
         if (snapshot.getAlarms() == null) {
         if (snapshot.getAlarms() == null) {
             return rows;
             return rows;
@@ -237,6 +255,9 @@ public class CockpitCurrentEventsService {
             if (status != null && !status.equals(alarm.getStatus())) {
             if (status != null && !status.equals(alarm.getStatus())) {
                 continue;
                 continue;
             }
             }
+            if (!CockpitDistrict.matches(district, alarm.getDistrict())) {
+                continue;
+            }
             rows.add(new EventRow(
             rows.add(new EventRow(
                     alarm.getAlarmId(),
                     alarm.getAlarmId(),
                     alarm.getWarningName(),
                     alarm.getWarningName(),

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

@@ -109,6 +109,8 @@ public class CockpitCurrentEventsSnapshot {
         private final BigDecimal longitude;
         private final BigDecimal longitude;
         private final BigDecimal latitude;
         private final BigDecimal latitude;
         private final boolean coordinateVerified;
         private final boolean coordinateVerified;
+        /** 事件片区(原始编码,未归一见 {@code CockpitDistrict});无设备/未识别时为 null=未分区。 */
+        private final Integer district;
 
 
         public SourceWarning(
         public SourceWarning(
                 String warningNo,
                 String warningNo,
@@ -128,7 +130,8 @@ public class CockpitCurrentEventsSnapshot {
                 String typeCode,
                 String typeCode,
                 BigDecimal longitude,
                 BigDecimal longitude,
                 BigDecimal latitude,
                 BigDecimal latitude,
-                boolean coordinateVerified) {
+                boolean coordinateVerified,
+                Integer district) {
             this.warningNo = warningNo;
             this.warningNo = warningNo;
             this.warningName = warningName;
             this.warningName = warningName;
             this.warningType = warningType;
             this.warningType = warningType;
@@ -147,6 +150,7 @@ public class CockpitCurrentEventsSnapshot {
             this.longitude = longitude;
             this.longitude = longitude;
             this.latitude = latitude;
             this.latitude = latitude;
             this.coordinateVerified = coordinateVerified;
             this.coordinateVerified = coordinateVerified;
+            this.district = district;
         }
         }
 
 
         public String getWarningNo() {
         public String getWarningNo() {
@@ -220,6 +224,11 @@ public class CockpitCurrentEventsSnapshot {
         public boolean isCoordinateVerified() {
         public boolean isCoordinateVerified() {
             return coordinateVerified;
             return coordinateVerified;
         }
         }
+
+        /** 片区原始编码;未经筛时也照实给出(调用方按 {@code CockpitDistrict} 归一)。 */
+        public Integer getDistrict() {
+            return district;
+        }
     }
     }
 
 
     public static class SourceAlarm {
     public static class SourceAlarm {
@@ -243,6 +252,8 @@ public class CockpitCurrentEventsSnapshot {
         private final boolean coordinateVerified;
         private final boolean coordinateVerified;
         private final BigDecimal thresholdMinValue;
         private final BigDecimal thresholdMinValue;
         private final BigDecimal thresholdMaxValue;
         private final BigDecimal thresholdMaxValue;
+        /** 报警片区(原始编码,未归一见 {@code CockpitDistrict});无设备/未识别时为 null=未分区。 */
+        private final Integer district;
 
 
         public SourceAlarm(
         public SourceAlarm(
                 String alarmId,
                 String alarmId,
@@ -264,7 +275,8 @@ public class CockpitCurrentEventsSnapshot {
                 BigDecimal latitude,
                 BigDecimal latitude,
                 boolean coordinateVerified,
                 boolean coordinateVerified,
                 BigDecimal thresholdMinValue,
                 BigDecimal thresholdMinValue,
-                BigDecimal thresholdMaxValue) {
+                BigDecimal thresholdMaxValue,
+                Integer district) {
             this.alarmId = alarmId;
             this.alarmId = alarmId;
             this.warningName = warningName;
             this.warningName = warningName;
             this.warningType = warningType;
             this.warningType = warningType;
@@ -285,6 +297,7 @@ public class CockpitCurrentEventsSnapshot {
             this.coordinateVerified = coordinateVerified;
             this.coordinateVerified = coordinateVerified;
             this.thresholdMinValue = thresholdMinValue;
             this.thresholdMinValue = thresholdMinValue;
             this.thresholdMaxValue = thresholdMaxValue;
             this.thresholdMaxValue = thresholdMaxValue;
+            this.district = district;
         }
         }
 
 
         public String getAlarmId() {
         public String getAlarmId() {
@@ -366,6 +379,11 @@ public class CockpitCurrentEventsSnapshot {
         public BigDecimal getThresholdMaxValue() {
         public BigDecimal getThresholdMaxValue() {
             return thresholdMaxValue;
             return thresholdMaxValue;
         }
         }
+
+        /** 片区原始编码;未经筛时也照实给出(调用方按 {@code CockpitDistrict} 归一)。 */
+        public Integer getDistrict() {
+            return district;
+        }
     }
     }
 
 
     public static class SourceWorkOrderLink {
     public static class SourceWorkOrderLink {

+ 10 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHandlingEfficiencyResponse.java

@@ -103,18 +103,27 @@ public class CockpitHandlingEfficiencyResponse {
         private final String industry;
         private final String industry;
         private final Integer level;
         private final Integer level;
         private final Integer workOrderState;
         private final Integer workOrderState;
+        private final Integer district;
+        private final String districtName;
 
 
-        public Filters(String eventFamily, String industry, Integer level, Integer workOrderState) {
+        public Filters(String eventFamily, String industry, Integer level, Integer workOrderState,
+                Integer district, String districtName) {
             this.eventFamily = eventFamily;
             this.eventFamily = eventFamily;
             this.industry = industry;
             this.industry = industry;
             this.level = level;
             this.level = level;
             this.workOrderState = workOrderState;
             this.workOrderState = workOrderState;
+            this.district = district;
+            this.districtName = districtName;
         }
         }
 
 
         public String getEventFamily() { return eventFamily; }
         public String getEventFamily() { return eventFamily; }
         public String getIndustry() { return industry; }
         public String getIndustry() { return industry; }
         public Integer getLevel() { return level; }
         public Integer getLevel() { return level; }
         public Integer getWorkOrderState() { return workOrderState; }
         public Integer getWorkOrderState() { return workOrderState; }
+        /** 生效片区编码(0-城南 1-城北 2-太常);未筛为 {@code null}。 */
+        public Integer getDistrict() { return district; }
+        /** 生效片区名:未筛为 {@code null},只看未分区时为「未分区」;双字段口径见 {@code CockpitDistrict}。 */
+        public String getDistrictName() { return districtName; }
     }
     }
 
 
     public static class Pagination {
     public static class Pagination {

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

@@ -1,6 +1,7 @@
 package com.zksy.cockpit.event;
 package com.zksy.cockpit.event;
 
 
 import com.zksy.base.warning.domain.WarningWorkflow;
 import com.zksy.base.warning.domain.WarningWorkflow;
+import com.zksy.cockpit.foundation.CockpitDistrict;
 import com.zksy.cockpit.foundation.CockpitErrorCategory;
 import com.zksy.cockpit.foundation.CockpitErrorCategory;
 import com.zksy.cockpit.foundation.CockpitErrorPayload;
 import com.zksy.cockpit.foundation.CockpitErrorPayload;
 import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
 import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
@@ -94,6 +95,7 @@ public class CockpitHandlingEfficiencyService {
             String industry,
             String industry,
             String level,
             String level,
             String workOrderState,
             String workOrderState,
+            String district,
             String startTime,
             String startTime,
             String endTime,
             String endTime,
             Integer page,
             Integer page,
@@ -102,6 +104,11 @@ public class CockpitHandlingEfficiencyService {
         String selectedIndustry = resolveIndustry(industry);
         String selectedIndustry = resolveIndustry(industry);
         Integer selectedLevel = resolveLevel(level);
         Integer selectedLevel = resolveLevel(level);
         Integer selectedState = resolveWorkOrderState(workOrderState);
         Integer selectedState = resolveWorkOrderState(workOrderState);
+        // 范围维度=片区(票 77):0/1/2 按片区筛、UNASSIGNED 只看未分区、不传=不筛;其它值 400。
+        CockpitDistrict.Query selectedDistrict = CockpitDistrict.parseQuery(district);
+        if (selectedDistrict.invalid()) {
+            throw new CockpitInvalidRequestException("district must be 0, 1, 2 or UNASSIGNED");
+        }
         TimeWindow window = resolveWindow(startTime, endTime);
         TimeWindow window = resolveWindow(startTime, endTime);
         int safePage = page == null ? 1 : page;
         int safePage = page == null ? 1 : page;
         int safePageSize = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
         int safePageSize = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
@@ -113,7 +120,7 @@ public class CockpitHandlingEfficiencyService {
                     : loaded;
                     : loaded;
         }
         }
         return build(requestId, family, selectedIndustry, selectedLevel, selectedState,
         return build(requestId, family, selectedIndustry, selectedLevel, selectedState,
-                window, safePage, safePageSize);
+                selectedDistrict, window, safePage, safePageSize);
     }
     }
 
 
     public CockpitErrorPayload invalidRequest(String requestId, String message) {
     public CockpitErrorPayload invalidRequest(String requestId, String message) {
@@ -135,6 +142,7 @@ public class CockpitHandlingEfficiencyService {
             String industry,
             String industry,
             Integer level,
             Integer level,
             Integer workOrderState,
             Integer workOrderState,
+            CockpitDistrict.Query district,
             TimeWindow window,
             TimeWindow window,
             int page,
             int page,
             int pageSize) {
             int pageSize) {
@@ -142,8 +150,8 @@ public class CockpitHandlingEfficiencyService {
         Map<String, Link> links = links(state.getWorkOrders());
         Map<String, Link> links = links(state.getWorkOrders());
         boolean warningFamily = WARNING.equals(family);
         boolean warningFamily = WARNING.equals(family);
         List<EventFact> facts = warningFamily
         List<EventFact> facts = warningFamily
-                ? warningFacts(state, types, industry, level)
-                : alarmFacts(state, types, industry, level);
+                ? warningFacts(state, types, industry, level, district)
+                : alarmFacts(state, types, industry, level, district);
         facts = inWindow(facts, window);
         facts = inWindow(facts, window);
 
 
         // 处置率口径(决策票 06 / 规格 10):分母=范围内**全部工单**(状态 0–6,
         // 处置率口径(决策票 06 / 规格 10):分母=范围内**全部工单**(状态 0–6,
@@ -158,7 +166,7 @@ public class CockpitHandlingEfficiencyService {
                 stateCounts.put(workOrderStateEntry.getCode(), 0);
                 stateCounts.put(workOrderStateEntry.getCode(), 0);
             }
             }
             for (CockpitHandlingEfficiencySnapshot.SourceWorkOrder order
             for (CockpitHandlingEfficiencySnapshot.SourceWorkOrder order
-                    : scopeWorkOrders(state, types, industry, window)) {
+                    : scopeWorkOrders(state, types, industry, district, window)) {
                 stateCounts.merge(resolveStateCode(order.getStateCode()), 1, Integer::sum);
                 stateCounts.merge(resolveStateCode(order.getStateCode()), 1, Integer::sum);
             }
             }
         }
         }
@@ -295,7 +303,10 @@ public class CockpitHandlingEfficiencyService {
                         timestamp(window.start),
                         timestamp(window.start),
                         timestamp(window.end)),
                         timestamp(window.end)),
                 isBlank(state.getUpdatedAt()) ? businessTime() : state.getUpdatedAt(),
                 isBlank(state.getUpdatedAt()) ? businessTime() : state.getUpdatedAt(),
-                new CockpitHandlingEfficiencyResponse.Filters(family, industry, level, workOrderState),
+                new CockpitHandlingEfficiencyResponse.Filters(
+                        family, industry, level, workOrderState,
+                        district.code(),
+                        district.active() ? CockpitDistrict.nameOf(district.code()) : null),
                 new CockpitHandlingEfficiencyResponse.Pagination(
                 new CockpitHandlingEfficiencyResponse.Pagination(
                         page, pageSize, total, totalPages, toIndex < total),
                         page, pageSize, total, totalPages, toIndex < total),
                 new CockpitHandlingEfficiencyResponse.MapFilter(
                 new CockpitHandlingEfficiencyResponse.MapFilter(
@@ -313,7 +324,7 @@ public class CockpitHandlingEfficiencyService {
                         warningDuration),
                         warningDuration),
                 stateRows,
                 stateRows,
                 warningStateRows,
                 warningStateRows,
-                durationTrend(state, family, types, industry, level, links, window),
+                durationTrend(state, family, types, industry, level, district, links, window),
                 items);
                 items);
     }
     }
 
 
@@ -449,6 +460,7 @@ public class CockpitHandlingEfficiencyService {
             Map<String, CockpitHandlingEfficiencySnapshot.SourceType> types,
             Map<String, CockpitHandlingEfficiencySnapshot.SourceType> types,
             String industry,
             String industry,
             Integer level,
             Integer level,
+            CockpitDistrict.Query district,
             Map<String, Link> links,
             Map<String, Link> links,
             TimeWindow window) {
             TimeWindow window) {
         ZonedDateTime first = window.start.atZoneSameInstant(BUSINESS_ZONE).truncatedTo(ChronoUnit.HOURS);
         ZonedDateTime first = window.start.atZoneSameInstant(BUSINESS_ZONE).truncatedTo(ChronoUnit.HOURS);
@@ -466,8 +478,8 @@ public class CockpitHandlingEfficiencyService {
         }
         }
         boolean warningFamily = WARNING.equals(family);
         boolean warningFamily = WARNING.equals(family);
         List<EventFact> facts = warningFamily
         List<EventFact> facts = warningFamily
-                ? warningFacts(snapshot, types, industry, level)
-                : alarmFacts(snapshot, types, industry, level);
+                ? warningFacts(snapshot, types, industry, level, district)
+                : alarmFacts(snapshot, types, industry, level, district);
         for (EventFact fact : facts) {
         for (EventFact fact : facts) {
             OffsetDateTime eventTime = parseEventTime(fact.eventTime);
             OffsetDateTime eventTime = parseEventTime(fact.eventTime);
             if (eventTime == null) {
             if (eventTime == null) {
@@ -507,7 +519,8 @@ public class CockpitHandlingEfficiencyService {
             CockpitHandlingEfficiencySnapshot snapshot,
             CockpitHandlingEfficiencySnapshot snapshot,
             Map<String, CockpitHandlingEfficiencySnapshot.SourceType> types,
             Map<String, CockpitHandlingEfficiencySnapshot.SourceType> types,
             String industry,
             String industry,
-            Integer level) {
+            Integer level,
+            CockpitDistrict.Query district) {
         List<EventFact> facts = new ArrayList<>();
         List<EventFact> facts = new ArrayList<>();
         if (snapshot.getWarnings() == null) {
         if (snapshot.getWarnings() == null) {
             return facts;
             return facts;
@@ -515,7 +528,8 @@ public class CockpitHandlingEfficiencyService {
         for (CockpitHandlingEfficiencySnapshot.SourceWarning warning : snapshot.getWarnings()) {
         for (CockpitHandlingEfficiencySnapshot.SourceWarning warning : snapshot.getWarnings()) {
             if (warning == null
             if (warning == null
                     || !inScope(types, warning.getTypeCode(), industry)
                     || !inScope(types, warning.getTypeCode(), industry)
-                    || (level != null && !level.equals(warning.getLevel()))) {
+                    || (level != null && !level.equals(warning.getLevel()))
+                    || !CockpitDistrict.matches(district, warning.getDistrict())) {
                 continue;
                 continue;
             }
             }
             facts.add(new EventFact(
             facts.add(new EventFact(
@@ -534,7 +548,8 @@ public class CockpitHandlingEfficiencyService {
             CockpitHandlingEfficiencySnapshot snapshot,
             CockpitHandlingEfficiencySnapshot snapshot,
             Map<String, CockpitHandlingEfficiencySnapshot.SourceType> types,
             Map<String, CockpitHandlingEfficiencySnapshot.SourceType> types,
             String industry,
             String industry,
-            Integer level) {
+            Integer level,
+            CockpitDistrict.Query district) {
         List<EventFact> facts = new ArrayList<>();
         List<EventFact> facts = new ArrayList<>();
         if (snapshot.getAlarms() == null) {
         if (snapshot.getAlarms() == null) {
             return facts;
             return facts;
@@ -542,7 +557,8 @@ public class CockpitHandlingEfficiencyService {
         for (CockpitHandlingEfficiencySnapshot.SourceAlarm alarm : snapshot.getAlarms()) {
         for (CockpitHandlingEfficiencySnapshot.SourceAlarm alarm : snapshot.getAlarms()) {
             if (alarm == null
             if (alarm == null
                     || !inScope(types, alarm.getTypeCode(), industry)
                     || !inScope(types, alarm.getTypeCode(), industry)
-                    || (level != null && !level.equals(alarm.getLevel()))) {
+                    || (level != null && !level.equals(alarm.getLevel()))
+                    || !CockpitDistrict.matches(district, alarm.getDistrict())) {
                 continue;
                 continue;
             }
             }
             facts.add(new EventFact(
             facts.add(new EventFact(
@@ -599,13 +615,16 @@ public class CockpitHandlingEfficiencyService {
             CockpitHandlingEfficiencySnapshot snapshot,
             CockpitHandlingEfficiencySnapshot snapshot,
             Map<String, CockpitHandlingEfficiencySnapshot.SourceType> types,
             Map<String, CockpitHandlingEfficiencySnapshot.SourceType> types,
             String industry,
             String industry,
+            CockpitDistrict.Query district,
             TimeWindow window) {
             TimeWindow window) {
         List<CockpitHandlingEfficiencySnapshot.SourceWorkOrder> result = new ArrayList<>();
         List<CockpitHandlingEfficiencySnapshot.SourceWorkOrder> result = new ArrayList<>();
         if (snapshot.getScopeWorkOrders() == null) {
         if (snapshot.getScopeWorkOrders() == null) {
             return result;
             return result;
         }
         }
         for (CockpitHandlingEfficiencySnapshot.SourceWorkOrder order : snapshot.getScopeWorkOrders()) {
         for (CockpitHandlingEfficiencySnapshot.SourceWorkOrder order : snapshot.getScopeWorkOrders()) {
-            if (order == null || !inScope(types, order.getTypeCode(), industry)) {
+            if (order == null
+                    || !inScope(types, order.getTypeCode(), industry)
+                    || !CockpitDistrict.matches(district, order.getDistrict())) {
                 continue;
                 continue;
             }
             }
             OffsetDateTime createTime = parseEventTime(order.getCreateTime());
             OffsetDateTime createTime = parseEventTime(order.getCreateTime());

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

@@ -69,18 +69,23 @@ public class CockpitHandlingEfficiencySnapshot {
         private final Integer stateCode;
         private final Integer stateCode;
         private final String createTime;
         private final String createTime;
         private final String typeCode;
         private final String typeCode;
+        /** 工单设备所属片区(原始编码);无设备/未识别时为 null=未分区(票 77)。 */
+        private final Integer district;
 
 
-        public SourceWorkOrder(String orderNo, Integer stateCode, String createTime, String typeCode) {
+        public SourceWorkOrder(String orderNo, Integer stateCode, String createTime, String typeCode,
+                Integer district) {
             this.orderNo = orderNo;
             this.orderNo = orderNo;
             this.stateCode = stateCode;
             this.stateCode = stateCode;
             this.createTime = createTime;
             this.createTime = createTime;
             this.typeCode = typeCode;
             this.typeCode = typeCode;
+            this.district = district;
         }
         }
 
 
         public String getOrderNo() { return orderNo; }
         public String getOrderNo() { return orderNo; }
         public Integer getStateCode() { return stateCode; }
         public Integer getStateCode() { return stateCode; }
         public String getCreateTime() { return createTime; }
         public String getCreateTime() { return createTime; }
         public String getTypeCode() { return typeCode; }
         public String getTypeCode() { return typeCode; }
+        public Integer getDistrict() { return district; }
     }
     }
 
 
     public static class SourceType {
     public static class SourceType {
@@ -113,6 +118,8 @@ public class CockpitHandlingEfficiencySnapshot {
         private final String status;
         private final String status;
         /** 预警记录最后更新时间:{@code CLOSED} 是终态且解除动作会写入该时间,故用作解除时刻来源。 */
         /** 预警记录最后更新时间:{@code CLOSED} 是终态且解除动作会写入该时间,故用作解除时刻来源。 */
         private final String updateTime;
         private final String updateTime;
+        /** 事件片区(原始编码,未归一见 {@code CockpitDistrict});无设备/未识别时为 null=未分区。 */
+        private final Integer district;
 
 
         public SourceWarning(
         public SourceWarning(
                 String warningNo,
                 String warningNo,
@@ -121,7 +128,7 @@ public class CockpitHandlingEfficiencySnapshot {
                 String typeCode,
                 String typeCode,
                 String deviceCode,
                 String deviceCode,
                 String deviceName) {
                 String deviceName) {
-            this(warningNo, level, eventTime, typeCode, deviceCode, deviceName, null, null);
+            this(warningNo, level, eventTime, typeCode, deviceCode, deviceName, null, null, null);
         }
         }
 
 
         public SourceWarning(
         public SourceWarning(
@@ -132,7 +139,8 @@ public class CockpitHandlingEfficiencySnapshot {
                 String deviceCode,
                 String deviceCode,
                 String deviceName,
                 String deviceName,
                 String status,
                 String status,
-                String updateTime) {
+                String updateTime,
+                Integer district) {
             this.warningNo = warningNo;
             this.warningNo = warningNo;
             this.level = level;
             this.level = level;
             this.eventTime = eventTime;
             this.eventTime = eventTime;
@@ -141,6 +149,7 @@ public class CockpitHandlingEfficiencySnapshot {
             this.deviceName = deviceName;
             this.deviceName = deviceName;
             this.status = status;
             this.status = status;
             this.updateTime = updateTime;
             this.updateTime = updateTime;
+            this.district = district;
         }
         }
 
 
         public String getWarningNo() { return warningNo; }
         public String getWarningNo() { return warningNo; }
@@ -151,6 +160,7 @@ public class CockpitHandlingEfficiencySnapshot {
         public String getDeviceName() { return deviceName; }
         public String getDeviceName() { return deviceName; }
         public String getStatus() { return status; }
         public String getStatus() { return status; }
         public String getUpdateTime() { return updateTime; }
         public String getUpdateTime() { return updateTime; }
+        public Integer getDistrict() { return district; }
     }
     }
 
 
     public static class SourceAlarm {
     public static class SourceAlarm {
@@ -160,6 +170,8 @@ public class CockpitHandlingEfficiencySnapshot {
         private final String typeCode;
         private final String typeCode;
         private final String deviceCode;
         private final String deviceCode;
         private final String deviceName;
         private final String deviceName;
+        /** 报警片区(原始编码,未归一见 {@code CockpitDistrict});无设备/未识别时为 null=未分区。 */
+        private final Integer district;
 
 
         public SourceAlarm(
         public SourceAlarm(
                 String alarmId,
                 String alarmId,
@@ -167,13 +179,15 @@ public class CockpitHandlingEfficiencySnapshot {
                 String eventTime,
                 String eventTime,
                 String typeCode,
                 String typeCode,
                 String deviceCode,
                 String deviceCode,
-                String deviceName) {
+                String deviceName,
+                Integer district) {
             this.alarmId = alarmId;
             this.alarmId = alarmId;
             this.level = level;
             this.level = level;
             this.eventTime = eventTime;
             this.eventTime = eventTime;
             this.typeCode = typeCode;
             this.typeCode = typeCode;
             this.deviceCode = deviceCode;
             this.deviceCode = deviceCode;
             this.deviceName = deviceName;
             this.deviceName = deviceName;
+            this.district = district;
         }
         }
 
 
         public String getAlarmId() { return alarmId; }
         public String getAlarmId() { return alarmId; }
@@ -182,6 +196,7 @@ public class CockpitHandlingEfficiencySnapshot {
         public String getTypeCode() { return typeCode; }
         public String getTypeCode() { return typeCode; }
         public String getDeviceCode() { return deviceCode; }
         public String getDeviceCode() { return deviceCode; }
         public String getDeviceName() { return deviceName; }
         public String getDeviceName() { return deviceName; }
+        public Integer getDistrict() { return district; }
     }
     }
 
 
     public static class SourceWorkOrderLink {
     public static class SourceWorkOrderLink {

+ 10 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/event/CockpitHistoryAnalysisResponse.java

@@ -102,16 +102,25 @@ public class CockpitHistoryAnalysisResponse {
         private final String eventFamily;
         private final String eventFamily;
         private final Integer level;
         private final Integer level;
         private final String industry;
         private final String industry;
+        private final Integer district;
+        private final String districtName;
 
 
-        public Filters(String eventFamily, Integer level, String industry) {
+        public Filters(String eventFamily, Integer level, String industry,
+                Integer district, String districtName) {
             this.eventFamily = eventFamily;
             this.eventFamily = eventFamily;
             this.level = level;
             this.level = level;
             this.industry = industry;
             this.industry = industry;
+            this.district = district;
+            this.districtName = districtName;
         }
         }
 
 
         public String getEventFamily() { return eventFamily; }
         public String getEventFamily() { return eventFamily; }
         public Integer getLevel() { return level; }
         public Integer getLevel() { return level; }
         public String getIndustry() { return industry; }
         public String getIndustry() { return industry; }
+        /** 生效片区编码(0-城南 1-城北 2-太常);未筛为 {@code null}。 */
+        public Integer getDistrict() { return district; }
+        /** 生效片区名:未筛为 {@code null},只看未分区时为「未分区」;双字段口径见 {@code CockpitDistrict}。 */
+        public String getDistrictName() { return districtName; }
     }
     }
 
 
     public static class Summary {
     public static class Summary {

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

@@ -1,5 +1,6 @@
 package com.zksy.cockpit.event;
 package com.zksy.cockpit.event;
 
 
+import com.zksy.cockpit.foundation.CockpitDistrict;
 import com.zksy.cockpit.foundation.CockpitErrorCategory;
 import com.zksy.cockpit.foundation.CockpitErrorCategory;
 import com.zksy.cockpit.foundation.CockpitErrorPayload;
 import com.zksy.cockpit.foundation.CockpitErrorPayload;
 import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
 import com.zksy.cockpit.foundation.CockpitInvalidRequestException;
@@ -70,6 +71,7 @@ public class CockpitHistoryAnalysisService {
             String eventFamily,
             String eventFamily,
             String level,
             String level,
             String industry,
             String industry,
+            String district,
             String startTime,
             String startTime,
             String endTime,
             String endTime,
             Integer page,
             Integer page,
@@ -77,6 +79,11 @@ public class CockpitHistoryAnalysisService {
         String family = resolveFamily(eventFamily);
         String family = resolveFamily(eventFamily);
         Integer resolvedLevel = resolveLevel(level);
         Integer resolvedLevel = resolveLevel(level);
         String selectedIndustry = resolveIndustry(industry);
         String selectedIndustry = resolveIndustry(industry);
+        // 范围维度=片区(票 77):0/1/2 按片区筛、UNASSIGNED 只看未分区、不传=不筛;其它值 400。
+        CockpitDistrict.Query selectedDistrict = CockpitDistrict.parseQuery(district);
+        if (selectedDistrict.invalid()) {
+            throw new CockpitInvalidRequestException("district must be 0, 1, 2 or UNASSIGNED");
+        }
         TimeWindow window = resolveWindow(startTime, endTime);
         TimeWindow window = resolveWindow(startTime, endTime);
         int safePage = page == null ? 1 : page;
         int safePage = page == null ? 1 : page;
         int safePageSize = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
         int safePageSize = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
@@ -87,7 +94,8 @@ public class CockpitHistoryAnalysisService {
                     ? CockpitHistoryAnalysisSnapshot.empty(businessTime())
                     ? CockpitHistoryAnalysisSnapshot.empty(businessTime())
                     : loaded;
                     : loaded;
         }
         }
-        return build(requestId, family, resolvedLevel, selectedIndustry, window, safePage, safePageSize);
+        return build(requestId, family, resolvedLevel, selectedIndustry, selectedDistrict,
+                window, safePage, safePageSize);
     }
     }
 
 
     public CockpitErrorPayload invalidRequest(String requestId, String message) {
     public CockpitErrorPayload invalidRequest(String requestId, String message) {
@@ -111,14 +119,15 @@ public class CockpitHistoryAnalysisService {
             String family,
             String family,
             Integer level,
             Integer level,
             String industry,
             String industry,
+            CockpitDistrict.Query district,
             TimeWindow window,
             TimeWindow window,
             int page,
             int page,
             int pageSize) {
             int pageSize) {
         Map<String, CockpitHistoryAnalysisSnapshot.SourceType> types = types(state);
         Map<String, CockpitHistoryAnalysisSnapshot.SourceType> types = types(state);
         Map<String, Integer> handledLinks = handledLinks(state.getWorkOrders());
         Map<String, Integer> handledLinks = handledLinks(state.getWorkOrders());
         List<EventRow> eventRows = WARNING.equals(family)
         List<EventRow> eventRows = WARNING.equals(family)
-                ? warningRows(state, types, level, industry)
-                : alarmRows(state, types, level, industry);
+                ? warningRows(state, types, level, industry, district)
+                : alarmRows(state, types, level, industry, district);
 
 
         ZonedDateTime first = window.start
         ZonedDateTime first = window.start
                 .atZoneSameInstant(BUSINESS_ZONE)
                 .atZoneSameInstant(BUSINESS_ZONE)
@@ -185,7 +194,7 @@ public class CockpitHistoryAnalysisService {
         String updatedAt = isBlank(state.getUpdatedAt()) ? businessTime() : state.getUpdatedAt();
         String updatedAt = isBlank(state.getUpdatedAt()) ? businessTime() : state.getUpdatedAt();
         // 汇总处置率=范围内全部工单(状态 0–6)口径(决策票 06),与行级事件桶分开;
         // 汇总处置率=范围内全部工单(状态 0–6)口径(决策票 06),与行级事件桶分开;
         // 行级桶仍是事件时间桶,其 efficiency 保留事件口径。
         // 行级桶仍是事件时间桶,其 efficiency 保留事件口径。
-        ScopeRate scopeRate = scopeRate(state, types, industry, window);
+        ScopeRate scopeRate = scopeRate(state, types, industry, district, window);
         return new CockpitHistoryAnalysisResponse(
         return new CockpitHistoryAnalysisResponse(
                 requestId,
                 requestId,
                 "v1",
                 "v1",
@@ -198,7 +207,10 @@ public class CockpitHistoryAnalysisService {
                         timestamp(window.start),
                         timestamp(window.start),
                         timestamp(window.end)),
                         timestamp(window.end)),
                 updatedAt,
                 updatedAt,
-                new CockpitHistoryAnalysisResponse.Filters(family, level, industry),
+                new CockpitHistoryAnalysisResponse.Filters(
+                        family, level, industry,
+                        district.code(),
+                        district.active() ? CockpitDistrict.nameOf(district.code()) : null),
                 new CockpitHistoryAnalysisResponse.Summary(
                 new CockpitHistoryAnalysisResponse.Summary(
                         totalEvents,
                         totalEvents,
                         // 保持事件口径,与行级 handledCount 同维(决策 q7:只改汇总处置率)。
                         // 保持事件口径,与行级 handledCount 同维(决策 q7:只改汇总处置率)。
@@ -215,7 +227,8 @@ public class CockpitHistoryAnalysisService {
             CockpitHistoryAnalysisSnapshot snapshot,
             CockpitHistoryAnalysisSnapshot snapshot,
             Map<String, CockpitHistoryAnalysisSnapshot.SourceType> types,
             Map<String, CockpitHistoryAnalysisSnapshot.SourceType> types,
             Integer level,
             Integer level,
-            String industry) {
+            String industry,
+            CockpitDistrict.Query district) {
         List<EventRow> rows = new ArrayList<>();
         List<EventRow> rows = new ArrayList<>();
         if (snapshot.getWarnings() == null) {
         if (snapshot.getWarnings() == null) {
             return rows;
             return rows;
@@ -223,7 +236,8 @@ public class CockpitHistoryAnalysisService {
         for (CockpitHistoryAnalysisSnapshot.SourceWarning warning : snapshot.getWarnings()) {
         for (CockpitHistoryAnalysisSnapshot.SourceWarning warning : snapshot.getWarnings()) {
             if (warning == null
             if (warning == null
                     || !inScope(types, warning.getTypeCode(), industry)
                     || !inScope(types, warning.getTypeCode(), industry)
-                    || (level != null && !level.equals(warning.getLevel()))) {
+                    || (level != null && !level.equals(warning.getLevel()))
+                    || !CockpitDistrict.matches(district, warning.getDistrict())) {
                 continue;
                 continue;
             }
             }
             OffsetDateTime eventTime = parseEventTime(warning.getEventTime());
             OffsetDateTime eventTime = parseEventTime(warning.getEventTime());
@@ -254,7 +268,8 @@ public class CockpitHistoryAnalysisService {
             CockpitHistoryAnalysisSnapshot snapshot,
             CockpitHistoryAnalysisSnapshot snapshot,
             Map<String, CockpitHistoryAnalysisSnapshot.SourceType> types,
             Map<String, CockpitHistoryAnalysisSnapshot.SourceType> types,
             Integer level,
             Integer level,
-            String industry) {
+            String industry,
+            CockpitDistrict.Query district) {
         List<EventRow> rows = new ArrayList<>();
         List<EventRow> rows = new ArrayList<>();
         if (snapshot.getAlarms() == null) {
         if (snapshot.getAlarms() == null) {
             return rows;
             return rows;
@@ -262,7 +277,8 @@ public class CockpitHistoryAnalysisService {
         for (CockpitHistoryAnalysisSnapshot.SourceAlarm alarm : snapshot.getAlarms()) {
         for (CockpitHistoryAnalysisSnapshot.SourceAlarm alarm : snapshot.getAlarms()) {
             if (alarm == null
             if (alarm == null
                     || !inScope(types, alarm.getTypeCode(), industry)
                     || !inScope(types, alarm.getTypeCode(), industry)
-                    || (level != null && !level.equals(alarm.getLevel()))) {
+                    || (level != null && !level.equals(alarm.getLevel()))
+                    || !CockpitDistrict.matches(district, alarm.getDistrict())) {
                 continue;
                 continue;
             }
             }
             OffsetDateTime eventTime = parseEventTime(alarm.getEventTime());
             OffsetDateTime eventTime = parseEventTime(alarm.getEventTime());
@@ -321,6 +337,7 @@ public class CockpitHistoryAnalysisService {
             CockpitHistoryAnalysisSnapshot snapshot,
             CockpitHistoryAnalysisSnapshot snapshot,
             Map<String, CockpitHistoryAnalysisSnapshot.SourceType> types,
             Map<String, CockpitHistoryAnalysisSnapshot.SourceType> types,
             String industry,
             String industry,
+            CockpitDistrict.Query district,
             TimeWindow window) {
             TimeWindow window) {
         int handled = 0;
         int handled = 0;
         int eligible = 0;
         int eligible = 0;
@@ -328,7 +345,9 @@ public class CockpitHistoryAnalysisService {
             return new ScopeRate(0, 0);
             return new ScopeRate(0, 0);
         }
         }
         for (CockpitHistoryAnalysisSnapshot.SourceWorkOrder order : snapshot.getScopeWorkOrders()) {
         for (CockpitHistoryAnalysisSnapshot.SourceWorkOrder order : snapshot.getScopeWorkOrders()) {
-            if (order == null || !inScope(types, order.getTypeCode(), industry)) {
+            if (order == null
+                    || !inScope(types, order.getTypeCode(), industry)
+                    || !CockpitDistrict.matches(district, order.getDistrict())) {
                 continue;
                 continue;
             }
             }
             OffsetDateTime createTime = parseEventTime(order.getCreateTime());
             OffsetDateTime createTime = parseEventTime(order.getCreateTime());

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

@@ -90,11 +90,14 @@ public class CockpitHistoryAnalysisSnapshot {
         private final Integer stateCode;
         private final Integer stateCode;
         private final String createTime;
         private final String createTime;
         private final String typeCode;
         private final String typeCode;
+        /** 工单设备所属片区(原始编码);无设备/未识别时为 null=未分区(票 77)。 */
+        private final Integer district;
 
 
-        public SourceWorkOrder(Integer stateCode, String createTime, String typeCode) {
+        public SourceWorkOrder(Integer stateCode, String createTime, String typeCode, Integer district) {
             this.stateCode = stateCode;
             this.stateCode = stateCode;
             this.createTime = createTime;
             this.createTime = createTime;
             this.typeCode = typeCode;
             this.typeCode = typeCode;
+            this.district = district;
         }
         }
 
 
         public Integer getStateCode() {
         public Integer getStateCode() {
@@ -108,6 +111,11 @@ public class CockpitHistoryAnalysisSnapshot {
         public String getTypeCode() {
         public String getTypeCode() {
             return typeCode;
             return typeCode;
         }
         }
+
+        /** 片区原始编码;未经筛时照实给出。 */
+        public Integer getDistrict() {
+            return district;
+        }
     }
     }
 
 
     public static class SourceType {
     public static class SourceType {
@@ -150,6 +158,8 @@ public class CockpitHistoryAnalysisSnapshot {
         private final String deviceName;
         private final String deviceName;
         private final String location;
         private final String location;
         private final String updatedTime;
         private final String updatedTime;
+        /** 事件片区(原始编码,未归一见 {@code CockpitDistrict});无设备/未识别时为 null=未分区。 */
+        private final Integer district;
 
 
         /** 聚合最小字段(既有测试/空适配器用)。 */
         /** 聚合最小字段(既有测试/空适配器用)。 */
         public SourceWarning(
         public SourceWarning(
@@ -158,7 +168,7 @@ public class CockpitHistoryAnalysisSnapshot {
                 String eventTime,
                 String eventTime,
                 String typeCode,
                 String typeCode,
                 String deviceCode) {
                 String deviceCode) {
-            this(warningNo, level, eventTime, typeCode, deviceCode, null, null, null, null);
+            this(warningNo, level, eventTime, typeCode, deviceCode, null, null, null, null, null);
         }
         }
 
 
         /** 票 61:逐事件行展示字段(业务适配器用)。 */
         /** 票 61:逐事件行展示字段(业务适配器用)。 */
@@ -171,7 +181,8 @@ public class CockpitHistoryAnalysisSnapshot {
                 String status,
                 String status,
                 String deviceName,
                 String deviceName,
                 String location,
                 String location,
-                String updatedTime) {
+                String updatedTime,
+                Integer district) {
             this.warningNo = warningNo;
             this.warningNo = warningNo;
             this.level = level;
             this.level = level;
             this.eventTime = eventTime;
             this.eventTime = eventTime;
@@ -181,6 +192,7 @@ public class CockpitHistoryAnalysisSnapshot {
             this.deviceName = deviceName;
             this.deviceName = deviceName;
             this.location = location;
             this.location = location;
             this.updatedTime = updatedTime;
             this.updatedTime = updatedTime;
+            this.district = district;
         }
         }
 
 
         public String getWarningNo() {
         public String getWarningNo() {
@@ -218,6 +230,11 @@ public class CockpitHistoryAnalysisSnapshot {
         public String getUpdatedTime() {
         public String getUpdatedTime() {
             return updatedTime;
             return updatedTime;
         }
         }
+
+        /** 片区原始编码(未经筛时照实给出,调用方按 {@code CockpitDistrict} 归一)。 */
+        public Integer getDistrict() {
+            return district;
+        }
     }
     }
 
 
     public static class SourceAlarm {
     public static class SourceAlarm {
@@ -235,6 +252,8 @@ public class CockpitHistoryAnalysisSnapshot {
         private final String unit;
         private final String unit;
         private final BigDecimal minValue;
         private final BigDecimal minValue;
         private final BigDecimal maxValue;
         private final BigDecimal maxValue;
+        /** 报警片区(原始编码,未归一见 {@code CockpitDistrict});无设备/未识别时为 null=未分区。 */
+        private final Integer district;
 
 
         /** 聚合最小字段(既有测试/空适配器用)。 */
         /** 聚合最小字段(既有测试/空适配器用)。 */
         public SourceAlarm(
         public SourceAlarm(
@@ -244,7 +263,7 @@ public class CockpitHistoryAnalysisSnapshot {
                 String typeCode,
                 String typeCode,
                 String deviceCode) {
                 String deviceCode) {
             this(alarmId, level, eventTime, typeCode, deviceCode,
             this(alarmId, level, eventTime, typeCode, deviceCode,
-                    null, null, null, null, null, null, null, null, null);
+                    null, null, null, null, null, null, null, null, null, null);
         }
         }
 
 
         /** 票 61:逐事件行展示字段(业务适配器用)。 */
         /** 票 61:逐事件行展示字段(业务适配器用)。 */
@@ -262,7 +281,8 @@ public class CockpitHistoryAnalysisSnapshot {
                 BigDecimal value,
                 BigDecimal value,
                 String unit,
                 String unit,
                 BigDecimal minValue,
                 BigDecimal minValue,
-                BigDecimal maxValue) {
+                BigDecimal maxValue,
+                Integer district) {
             this.alarmId = alarmId;
             this.alarmId = alarmId;
             this.level = level;
             this.level = level;
             this.eventTime = eventTime;
             this.eventTime = eventTime;
@@ -277,6 +297,7 @@ public class CockpitHistoryAnalysisSnapshot {
             this.unit = unit;
             this.unit = unit;
             this.minValue = minValue;
             this.minValue = minValue;
             this.maxValue = maxValue;
             this.maxValue = maxValue;
+            this.district = district;
         }
         }
 
 
         public String getAlarmId() {
         public String getAlarmId() {
@@ -334,6 +355,11 @@ public class CockpitHistoryAnalysisSnapshot {
         public BigDecimal getMaxValue() {
         public BigDecimal getMaxValue() {
             return maxValue;
             return maxValue;
         }
         }
+
+        /** 片区原始编码(未经筛时照实给出,调用方按 {@code CockpitDistrict} 归一)。 */
+        public Integer getDistrict() {
+            return district;
+        }
     }
     }
 
 
     public static class SourceWorkOrderLink {
     public static class SourceWorkOrderLink {

+ 45 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/BusinessCockpitWorkOrderReadAdapter.java

@@ -7,12 +7,15 @@ import com.zksy.ops.service.WorkOrderService;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.alarm.service.AlarmDataService;
 import com.zksy.base.alarm.service.AlarmDataService;
 import com.zksy.base.domain.EquipmentBase;
 import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentType;
 import com.zksy.base.mapper.EquipmentBaseMapper;
 import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.service.EquipmentTypeService;
 import com.zksy.base.warning.domain.AlarmWarningRel;
 import com.zksy.base.warning.domain.AlarmWarningRel;
 import com.zksy.base.warning.domain.EarlyWarning;
 import com.zksy.base.warning.domain.EarlyWarning;
 import com.zksy.base.warning.mapper.AlarmWarningRelMapper;
 import com.zksy.base.warning.mapper.AlarmWarningRelMapper;
 import com.zksy.base.warning.service.EarlyWarningService;
 import com.zksy.base.warning.service.EarlyWarningService;
 import com.zksy.cockpit.foundation.CockpitReadException;
 import com.zksy.cockpit.foundation.CockpitReadException;
+import com.zksy.cockpit.foundation.CockpitTypeIndustry;
 import org.apache.ibatis.exceptions.PersistenceException;
 import org.apache.ibatis.exceptions.PersistenceException;
 import org.springframework.context.annotation.Primary;
 import org.springframework.context.annotation.Primary;
 import org.springframework.dao.DataAccessException;
 import org.springframework.dao.DataAccessException;
@@ -20,6 +23,7 @@ import org.springframework.stereotype.Component;
 
 
 import java.time.ZoneId;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Date;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
@@ -38,18 +42,21 @@ public class BusinessCockpitWorkOrderReadAdapter implements CockpitWorkOrderRead
     private final AlarmWarningRelMapper alarmWarningRelMapper;
     private final AlarmWarningRelMapper alarmWarningRelMapper;
     private final AlarmDataService alarmDataService;
     private final AlarmDataService alarmDataService;
     private final EquipmentBaseMapper equipmentBaseMapper;
     private final EquipmentBaseMapper equipmentBaseMapper;
+    private final EquipmentTypeService equipmentTypeService;
 
 
     public BusinessCockpitWorkOrderReadAdapter(
     public BusinessCockpitWorkOrderReadAdapter(
             WorkOrderService workOrderService,
             WorkOrderService workOrderService,
             EarlyWarningService earlyWarningService,
             EarlyWarningService earlyWarningService,
             AlarmWarningRelMapper alarmWarningRelMapper,
             AlarmWarningRelMapper alarmWarningRelMapper,
             AlarmDataService alarmDataService,
             AlarmDataService alarmDataService,
-            EquipmentBaseMapper equipmentBaseMapper) {
+            EquipmentBaseMapper equipmentBaseMapper,
+            EquipmentTypeService equipmentTypeService) {
         this.workOrderService = workOrderService;
         this.workOrderService = workOrderService;
         this.earlyWarningService = earlyWarningService;
         this.earlyWarningService = earlyWarningService;
         this.alarmWarningRelMapper = alarmWarningRelMapper;
         this.alarmWarningRelMapper = alarmWarningRelMapper;
         this.alarmDataService = alarmDataService;
         this.alarmDataService = alarmDataService;
         this.equipmentBaseMapper = equipmentBaseMapper;
         this.equipmentBaseMapper = equipmentBaseMapper;
+        this.equipmentTypeService = equipmentTypeService;
     }
     }
 
 
     @Override
     @Override
@@ -102,6 +109,7 @@ public class BusinessCockpitWorkOrderReadAdapter implements CockpitWorkOrderRead
             Integer stateCode,
             Integer stateCode,
             Date startTime,
             Date startTime,
             Date endTime,
             Date endTime,
+            String industry,
             int limit,
             int limit,
             int offset) {
             int offset) {
         LambdaQueryWrapper<WorkOrder> wrapper = new LambdaQueryWrapper<>();
         LambdaQueryWrapper<WorkOrder> wrapper = new LambdaQueryWrapper<>();
@@ -114,6 +122,22 @@ public class BusinessCockpitWorkOrderReadAdapter implements CockpitWorkOrderRead
         if (endTime != null) {
         if (endTime != null) {
             wrapper.le(WorkOrder::getCreateTime, endTime);
             wrapper.le(WorkOrder::getCreateTime, endTime);
         }
         }
+        if (industry != null) {
+            List<String> typeIds = typeIdsOfIndustry(industry);
+            if (typeIds.isEmpty()) {
+                // 该行业在类型树上没有任何设备类型 ⇒ 不可能有工单命中。
+                // 直接回空页,不把空的 IN 条件下到 SQL。
+                return new CockpitWorkOrderListSnapshot(
+                        Collections.emptyList(), 0,
+                        limit <= 0 ? 1 : (offset / limit) + 1, limit, stateCode);
+            }
+            // 工单 → 设备 → 设备类型 → 行业:与 CockpitHandlingEfficiencyService.scopeWorkOrders(...)
+            // 同一套派生,两处筛的必须是同一批工单(票 65 的纪律)。设备表在默认 search_path 上,
+            // 用子查询而不是把上千个设备 ID 塞进 IN 列表。
+            wrapper.inSql(WorkOrder::getDeviceId,
+                    "SELECT equipment_id FROM equipment_base WHERE equipment_type_id IN ("
+                            + String.join(", ", typeIds) + ")");
+        }
         long total = workOrderService.count(wrapper);
         long total = workOrderService.count(wrapper);
         List<WorkOrder> orders = total <= offset
         List<WorkOrder> orders = total <= offset
                 ? Collections.emptyList()
                 ? Collections.emptyList()
@@ -304,6 +328,26 @@ public class BusinessCockpitWorkOrderReadAdapter implements CockpitWorkOrderRead
                 associatedWithCurrentEvent);
                 associatedWithCurrentEvent);
     }
     }
 
 
+    /**
+     * 行业 → 该行业在设备类型树上的全部类型 ID(含叶子;行业沿父级上溯判定,规则单源
+     * {@link CockpitTypeIndustry})。该行业没有登记任何类型时回空列表。
+     */
+    private List<String> typeIdsOfIndustry(String industry) {
+        Map<Integer, EquipmentType> types = new LinkedHashMap<>();
+        for (EquipmentType type : equipmentTypeService.list()) {
+            if (type != null && type.getId() != null) {
+                types.put(type.getId(), type);
+            }
+        }
+        List<String> ids = new ArrayList<>();
+        for (EquipmentType type : types.values()) {
+            if (industry.equals(CockpitTypeIndustry.industryOf(type, types))) {
+                ids.add(String.valueOf(type.getId()));
+            }
+        }
+        return ids;
+    }
+
     private String resolveDeviceCode(String deviceId) {
     private String resolveDeviceCode(String deviceId) {
         EquipmentBase equipment = deviceId == null ? null : equipmentBaseMapper.selectById(deviceId);
         EquipmentBase equipment = deviceId == null ? null : equipmentBaseMapper.selectById(deviceId);
         return equipment == null ? null : equipment.getEquipmentCode();
         return equipment == null ? null : equipment.getEquipmentCode();

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

@@ -24,6 +24,10 @@ public interface CockpitWorkOrderReadAdapter {
      * @param stateCode 工单状态码 0–7;{@code null} 表示不筛
      * @param stateCode 工单状态码 0–7;{@code null} 表示不筛
      * @param startTime 窗口起点(含);{@code null} 表示不限下界
      * @param startTime 窗口起点(含);{@code null} 表示不限下界
      * @param endTime   窗口终点(含);{@code null} 表示不限上界
      * @param endTime   窗口终点(含);{@code null} 表示不限上界
+     * @param industry  行业编码(GAS/WATER_SUPPLY/DRAINAGE/MANHOLE 之一);{@code null} 表示不筛(票 77):
+     *                  口径经「工单设备 → 设备类型 → 行业」派生,与
+     *                  {@code CockpitHandlingEfficiencyService.scopeWorkOrders(...)} 同一套规则,
+     *                  这样列表与上方状态分布筛的是**同一批工单**。
      * @param limit     本页条数(调用方已夹到合法区间)
      * @param limit     本页条数(调用方已夹到合法区间)
      * @param offset    偏移量,{@code (page-1)*limit}
      * @param offset    偏移量,{@code (page-1)*limit}
      */
      */
@@ -31,6 +35,7 @@ public interface CockpitWorkOrderReadAdapter {
             Integer stateCode,
             Integer stateCode,
             Date startTime,
             Date startTime,
             Date endTime,
             Date endTime,
+            String industry,
             int limit,
             int limit,
             int offset);
             int offset);
 }
 }

+ 35 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/workorder/CockpitWorkOrderReadService.java

@@ -21,6 +21,10 @@ import java.util.List;
 public class CockpitWorkOrderReadService {
 public class CockpitWorkOrderReadService {
     private static final List<String> VALID_EVENT_SOURCES = Arrays.asList("ALARM", "WARNING");
     private static final List<String> VALID_EVENT_SOURCES = Arrays.asList("ALARM", "WARNING");
 
 
+    /** 正式行业域(与事件族端点同一份词表;票 77 为 /work-orders 补上行业筛选)。 */
+    private static final List<String> INDUSTRIES =
+            Arrays.asList("GAS", "WATER_SUPPLY", "DRAINAGE", "MANHOLE");
+
     /** 全量工单列表的默认页大小(与驾驶舱其它列表端点一致)。 */
     /** 全量工单列表的默认页大小(与驾驶舱其它列表端点一致)。 */
     private static final int DEFAULT_PAGE_SIZE = 10;
     private static final int DEFAULT_PAGE_SIZE = 10;
 
 
@@ -70,7 +74,15 @@ public class CockpitWorkOrderReadService {
     }
     }
 
 
     public CockpitErrorPayload invalidRequest(String requestId) {
     public CockpitErrorPayload invalidRequest(String requestId) {
-        return error(requestId, CockpitErrorCategory.INVALID_REQUEST, "工单查询参数不合法");
+        return invalidRequest(requestId, "工单查询参数不合法");
+    }
+
+    /**
+     * 带原因的入参错误(票 77):与事件族端点同一纪律 —— 非法值要能说清哪一条不合法,
+     * 前端据此提示、排障时也能直接看出被拒的是哪个参数。
+     */
+    public CockpitErrorPayload invalidRequest(String requestId, String message) {
+        return error(requestId, CockpitErrorCategory.INVALID_REQUEST, message);
     }
     }
 
 
     /**
     /**
@@ -92,8 +104,10 @@ public class CockpitWorkOrderReadService {
             Integer page,
             Integer page,
             Integer pageSize,
             Integer pageSize,
             Integer stateCode,
             Integer stateCode,
+            String industry,
             String startTime,
             String startTime,
             String endTime) {
             String endTime) {
+        String safeIndustry = resolveIndustry(industry);
         int safePage = page == null || page < 1 ? 1 : page;
         int safePage = page == null || page < 1 ? 1 : page;
         int safeSize = pageSize == null || pageSize < 1 ? DEFAULT_PAGE_SIZE : Math.min(pageSize, MAX_PAGE_SIZE);
         int safeSize = pageSize == null || pageSize < 1 ? DEFAULT_PAGE_SIZE : Math.min(pageSize, MAX_PAGE_SIZE);
         Integer safeState = stateCode != null && stateCode >= 0 && stateCode <= 7 ? stateCode : null;
         Integer safeState = stateCode != null && stateCode >= 0 && stateCode <= 7 ? stateCode : null;
@@ -103,7 +117,8 @@ public class CockpitWorkOrderReadService {
             throw new CockpitInvalidRequestException("startTime must not be after endTime");
             throw new CockpitInvalidRequestException("startTime must not be after endTime");
         }
         }
         CockpitWorkOrderListSnapshot snapshot = adapter.loadAll(
         CockpitWorkOrderListSnapshot snapshot = adapter.loadAll(
-                safeState, windowStart, windowEnd, safeSize, (safePage - 1) * safeSize);
+                safeState, windowStart, windowEnd, safeIndustry,
+                safeSize, (safePage - 1) * safeSize);
         if (snapshot == null) {
         if (snapshot == null) {
             throw new CockpitReadException("work order list unavailable");
             throw new CockpitReadException("work order list unavailable");
         }
         }
@@ -121,6 +136,24 @@ public class CockpitWorkOrderReadService {
                 new CockpitWorkOrderListResponse.Pagination(safePage, safeSize, snapshot.getTotal()));
                 new CockpitWorkOrderListResponse.Pagination(safePage, safeSize, snapshot.getTotal()));
     }
     }
 
 
+    /**
+     * 行业入参:空/null=不筛;不在正式行业域内一律 400。
+     *
+     * <p>为什么不容错成「忽略这个参数」:静默忽略会造出「以为按行业筛了、其实是全量」的假绿,
+     * 与票 65 补时间窗时的同一条纪律(非法值一律 400)。
+     */
+    private static String resolveIndustry(String industry) {
+        if (industry == null || industry.trim().isEmpty()) {
+            return null;
+        }
+        String value = industry.trim();
+        if (!INDUSTRIES.contains(value)) {
+            throw new CockpitInvalidRequestException(
+                    "industry must be GAS, WATER_SUPPLY, DRAINAGE or MANHOLE");
+        }
+        return value;
+    }
+
     /**
     /**
      * 时间窗口端点 → {@link Date}(ISO-8601,必须带时区偏移)。
      * 时间窗口端点 → {@link Date}(ISO-8601,必须带时区偏移)。
      *
      *

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

@@ -24,6 +24,7 @@ public class EmptyCockpitWorkOrderReadAdapter implements CockpitWorkOrderReadAda
             Integer stateCode,
             Integer stateCode,
             java.util.Date startTime,
             java.util.Date startTime,
             java.util.Date endTime,
             java.util.Date endTime,
+            String industry,
             int limit,
             int limit,
             int offset) {
             int offset) {
         return null;
         return null;