Quellcode durchsuchen

feat(water): complete device admission and abnormal ledger

Kazerin vor 1 Tag
Ursprung
Commit
c654d3f9ef
17 geänderte Dateien mit 1944 neuen und 245 gelöschten Zeilen
  1. 145 0
      pipe-network-service/sql/water_supply_device_test_data.sql
  2. 57 6
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceAbnormal/DeviceAbnormalController.java
  3. 19 7
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceLedger/DeviceLedgerController.java
  4. 29 4
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceMaintain/DeviceMaintainController.java
  5. 156 0
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/DeviceAbnormal/DeviceAbnormalControllerTest.java
  6. 50 0
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/DeviceLedger/DeviceLedgerControllerTest.java
  7. 76 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterDeviceAbnormalExportDTO.java
  8. 51 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterDeviceLedgerExportDTO.java
  9. 15 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/AbnormalDeviceVO.java
  10. 15 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/DeviceDetailVO.java
  11. 6 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/MaintainOrderVO.java
  12. 17 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/WaterSupplyDeviceService.java
  13. 38 9
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/BusinessWorkOrderServiceImpl.java
  14. 514 217
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WaterSupplyDeviceServiceImpl.java
  15. 4 1
      pipe-network-service/zksy-system/src/main/resources/mapper/system/SysUserMapper.xml
  16. 62 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/service/impl/BusinessWorkOrderServiceImplTest.java
  17. 690 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/base/service/impl/WaterSupplyDeviceServiceImplTest.java

+ 145 - 0
pipe-network-service/sql/water_supply_device_test_data.sql

@@ -0,0 +1,145 @@
+-- ============================================================
+-- 供水管网 T6 设备准入、台账与异常态势 测试数据脚本(KingbaseES)
+-- 说明:
+--   1. 测试主键与编号均使用 T6-TEST- 前缀,可重复执行。
+--   2. 本脚本只初始化测试数据,不创建业务表;执行前请确认以下表已存在:
+--      app_user.equipment_type、app_user.equipment_base、app_user.equipment_status、
+--      app_user.alarm_data、app_user.work_order
+--   3. 覆盖场景:正常供水设备、报警供水设备、维修供水设备、离线供水设备、
+--      非供水设备排除、有效报警原因、真实工单编号与状态。
+-- ============================================================
+
+-- ------------------------------------------------------------
+-- 0. 清理历史测试数据(先清工单与报警,再清状态与设备,最后清临时类型)
+-- ------------------------------------------------------------
+DELETE FROM app_user.work_order_log WHERE order_id IN
+    (SELECT order_id FROM app_user.work_order WHERE order_no LIKE 'T6-TEST-WO-%');
+DELETE FROM app_user.work_order WHERE order_no LIKE 'T6-TEST-WO-%';
+DELETE FROM app_user.alarm_data WHERE id LIKE 'T6-TEST-ALARM-%';
+DELETE FROM app_user.equipment_status WHERE equipment_id LIKE 'T6-TEST-EQ-%';
+DELETE FROM app_user.equipment_base WHERE equipment_id LIKE 'T6-TEST-EQ-%';
+DELETE FROM app_user.equipment_type WHERE id = 'T6-TEST-GAS-TYPE';
+
+-- ------------------------------------------------------------
+-- 1. 校验规范供水设备类型,并创建临时非供水类型用于边界排除
+-- ------------------------------------------------------------
+DO $$
+DECLARE
+    v_count integer;
+BEGIN
+    SELECT count(*) INTO v_count
+      FROM app_user.equipment_type
+     WHERE id = '1' AND parent_type_id = '0' AND type_name = '供水';
+    IF v_count <> 1 THEN
+        RAISE EXCEPTION '缺少规范供水顶级设备类型:id=1,type_name=供水,parent_type_id=0';
+    END IF;
+
+    SELECT count(*) INTO v_count
+      FROM app_user.equipment_type
+     WHERE type_id IN ('ws_flow', 'ws_pressure', 'ws_leak')
+       AND parent_type_id = '1';
+    IF v_count <> 3 THEN
+        RAISE EXCEPTION '缺少规范供水监测子类型:ws_flow/ws_pressure/ws_leak 必须 parent_type_id=1';
+    END IF;
+END $$;
+
+INSERT INTO app_user.equipment_type
+    (id, type_id, type_name, parent_type_id, type_remark)
+VALUES
+    ('T6-TEST-GAS-TYPE', 't6_test_gas', '燃气(测试)', '0', 'T6非供水设备排除测试类型');
+
+-- ------------------------------------------------------------
+-- 2. 设备基础信息(4台供水设备 + 1台非供水设备)
+-- ------------------------------------------------------------
+INSERT INTO app_user.equipment_base
+    (equipment_id, equipment_code, equipment_name, equipment_model, equipment_type_id,
+     longitude, latitude, equipment_location, maintainer, maintainer_phone,
+     ownership_unit, remark, create_time, update_time, del_flag, district)
+VALUES
+    ('T6-TEST-EQ-PRESSURE-ALARM', 'CN-GS-202609020001', 'T6测试压力报警设备', 'PRESSURE-T6',
+     (SELECT id FROM app_user.equipment_type WHERE type_id = 'ws_pressure' AND parent_type_id = '1' LIMIT 1),
+     113.28000000, 23.12000000, 'T6测试大道1号', 'T6测试维护员', '13800000001',
+     'T6测试供水公司', 'T6设备准入与异常态势测试数据', now() - interval '7 days', now(), '0', '城南'),
+    ('T6-TEST-EQ-FLOW-REPAIR', 'CB-GS-202609020001', 'T6测试流量维修设备', 'FLOW-T6',
+     (SELECT id FROM app_user.equipment_type WHERE type_id = 'ws_flow' AND parent_type_id = '1' LIMIT 1),
+     113.29000000, 23.13000000, 'T6测试大道2号', 'T6测试维护员', '13800000002',
+     'T6测试供水公司', 'T6设备准入与异常态势测试数据', now() - interval '7 days', now(), '0', '城北'),
+    ('T6-TEST-EQ-LEAK-OFFLINE', 'TC-GS-202609020001', 'T6测试漏失离线设备', 'LEAK-T6',
+     (SELECT id FROM app_user.equipment_type WHERE type_id = 'ws_leak' AND parent_type_id = '1' LIMIT 1),
+     113.30000000, 23.14000000, 'T6测试大道3号', 'T6测试维护员', '13800000003',
+     'T6测试供水公司', 'T6设备准入与异常态势测试数据', now() - interval '7 days', now(), '0', '太常'),
+    ('T6-TEST-EQ-PRESSURE-NORMAL', 'CN-GS-202609020002', 'T6测试压力正常设备', 'PRESSURE-T6',
+     (SELECT id FROM app_user.equipment_type WHERE type_id = 'ws_pressure' AND parent_type_id = '1' LIMIT 1),
+     113.31000000, 23.15000000, 'T6测试大道4号', 'T6测试维护员', '13800000004',
+     'T6测试供水公司', 'T6设备准入与异常态势测试数据', now() - interval '7 days', now(), '0', '城南'),
+    ('T6-TEST-EQ-GAS-EXCLUDED', 'CN-RQ-202609020001', 'T6测试燃气排除设备', 'GAS-T6',
+     'T6-TEST-GAS-TYPE', 113.32000000, 23.16000000, 'T6测试大道5号',
+     'T6测试维护员', '13800000005', 'T6测试燃气公司', 'T6非供水排除测试数据',
+     now() - interval '7 days', now(), '0', '城南');
+
+-- ------------------------------------------------------------
+-- 3. 设备状态(current_status / alarm_status / online_status 分别表达不同状态)
+-- ------------------------------------------------------------
+INSERT INTO app_user.equipment_status
+    (equipment_id, current_status, alarm_status, online_status,
+     installation_addr, status_update_time, create_time, del_flag)
+VALUES
+    ('T6-TEST-EQ-PRESSURE-ALARM', 1, 1, 1, 'T6测试大道1号',
+     now() - interval '1 hour', now() - interval '1 hour', '0'),
+    ('T6-TEST-EQ-FLOW-REPAIR', 3, 0, 0, 'T6测试大道2号',
+     now() - interval '2 hours', now() - interval '2 hours', '0'),
+    ('T6-TEST-EQ-LEAK-OFFLINE', 1, 0, 0, 'T6测试大道3号',
+     now() - interval '3 hours', now() - interval '3 hours', '0'),
+    ('T6-TEST-EQ-PRESSURE-NORMAL', 1, 0, 1, 'T6测试大道4号',
+     now() - interval '4 hours', now() - interval '4 hours', '0'),
+    ('T6-TEST-EQ-GAS-EXCLUDED', 1, 1, 1, 'T6测试大道5号',
+     now() - interval '5 hours', now() - interval '5 hours', '0');
+
+-- ------------------------------------------------------------
+-- 4. 有效报警原因(供水报警设备 + 非供水排除设备)
+-- ------------------------------------------------------------
+INSERT INTO app_user.alarm_data
+    (id, device_code, device_name, device_type, warning_type, warning_code,
+     min_value, max_value, actual_value, alarm_level, deviation_ratio,
+     alarm_status, alarm_time, handle_time, handle_user, handle_remark, remark,
+     create_time, update_time)
+VALUES
+    ('T6-TEST-ALARM-PRESSURE', 'CN-GS-202609020001', 'T6测试压力报警设备', '压力监测',
+     '供水压力超限', 'WARN-PRESSURE-T6', 0.20, 0.80, 1.20, 2, 50.00,
+     0, now() - interval '1 hour', NULL, NULL, NULL, 'T6设备异常态势测试数据',
+     now() - interval '1 hour', now() - interval '1 hour'),
+    ('T6-TEST-ALARM-GAS-EXCLUDED', 'CN-RQ-202609020001', 'T6测试燃气排除设备', '燃气监测',
+     '燃气浓度超限', 'WARN-GAS-T6', 0.00, 20.00, 45.00, 1, 125.00,
+     0, now() - interval '1 hour', NULL, NULL, NULL, 'T6非供水排除测试数据',
+     now() - interval '1 hour', now() - interval '1 hour');
+
+-- ------------------------------------------------------------
+-- 5. 关联统一工单(真实 order_no,不用随机 ID)
+-- ------------------------------------------------------------
+INSERT INTO app_user.work_order
+    (order_no, alarm_id, case_id, case_no, device_id, device_code,
+     order_type, order_level, order_desc, dept_id, dispatch_user, dispatch_time,
+     receive_user, order_status, delayed_reason, delayed_deadline, plan_finish_time,
+     real_finish_time, repair_result, maintenance_before_img, maintenance_after_img,
+     verify_user, verify_time, create_time, update_time)
+VALUES
+    ('T6-TEST-WO-001', 'T6-TEST-ALARM-PRESSURE', NULL, NULL,
+     'T6-TEST-EQ-PRESSURE-ALARM', 'CN-GS-202609020001',
+     1, 2, 'T6供水压力超限故障维修', NULL, NULL, now() - interval '50 minutes',
+     '1', 4, NULL, NULL, now() + interval '1 day',
+     NULL, NULL, NULL, NULL, NULL, NULL, now() - interval '50 minutes', now()),
+    ('T6-TEST-WO-002', NULL, NULL, NULL,
+     'T6-TEST-EQ-FLOW-REPAIR', 'CB-GS-202609020001',
+     1, 2, 'T6供水流量设备维修', NULL, NULL, now() - interval '2 hours',
+     '2', 4, NULL, NULL, now() + interval '1 day',
+     NULL, NULL, NULL, NULL,
+     NULL, NULL, now() - interval '2 hours', now());
+
+-- ============================================================
+-- 快速核验:
+--   1. 供水设备台账应包含 4 台 T6 供水设备,不包含 CN-RQ-202609020001。
+--   2. 异常设备页应包含压力报警设备与流量维修设备;正常压力设备与漏失离线设备不进入异常页。
+--   3. CN-GS-202609020001 的异常原因为“供水压力超限”,关联工单号为 T6-TEST-WO-001,状态为处理中。
+--   4. CB-GS-202609020001 的异常原因为设备维修中,关联工单号为 T6-TEST-WO-002,状态为处理中。
+--   5. CN-RQ-202609020001 属于燃气测试类型,任何供水页面均不应展示。
+-- ============================================================

+ 57 - 6
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceAbnormal/DeviceAbnormalController.java

@@ -2,10 +2,17 @@ package com.zksy.web.controller.WaterSupply.DeviceAbnormal;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zksy.base.domain.vo.AbnormalDeviceVO;
+import com.zksy.base.domain.vo.MaintainOrderVO;
+import com.zksy.base.domain.WorkOrder;
 import com.zksy.base.service.WaterSupplyDeviceService;
+import com.zksy.base.service.BusinessWorkOrderService;
+import com.zksy.WaterSupply.export.WaterDeviceAbnormalExportDTO;
 import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.enums.BusinessType;
+import com.zksy.common.utils.poi.ExcelUtil;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.common.exception.ServiceException;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -14,7 +21,9 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletResponse;
 import java.util.List;
+import java.util.stream.Collectors;
 
 @Slf4j
 @RestController
@@ -25,6 +34,9 @@ public class DeviceAbnormalController {
     @Autowired
     private WaterSupplyDeviceService waterSupplyDeviceService;
 
+    @Autowired
+    private BusinessWorkOrderService businessWorkOrderService;
+
     @GetMapping("/page")
     @ApiOperation("异常设备分页查询")
     @PreAuthorize("@ss.hasPermi('waterSupply:device:abnormal:list')")
@@ -34,9 +46,11 @@ public class DeviceAbnormalController {
             @ApiParam("设备名称") @RequestParam(required = false) String equipmentName,
             @ApiParam("设备编码") @RequestParam(required = false) String equipmentCode,
             @ApiParam("设备类型ID") @RequestParam(required = false) String equipmentTypeId,
-            @ApiParam("告警等级 1-4") @RequestParam(required = false) Integer alertLevel) {
+            @ApiParam("告警等级 1-4") @RequestParam(required = false) Integer alertLevel,
+            @ApiParam("仅返回没有未结束故障维修工单的可派单设备")
+            @RequestParam(required = false, defaultValue = "false") Boolean dispatchableOnly) {
         Page<AbnormalDeviceVO> page = waterSupplyDeviceService.getAbnormalDevicePage(
-                pageNum, pageSize, equipmentName, equipmentCode, equipmentTypeId, alertLevel);
+                pageNum, pageSize, equipmentName, equipmentCode, equipmentTypeId, alertLevel, dispatchableOnly);
         return AjaxResult.success(page);
     }
 
@@ -49,17 +63,54 @@ public class DeviceAbnormalController {
         return AjaxResult.success(list);
     }
 
+    @PostMapping("/order")
+    @ApiOperation("供水异常设备创建故障维修工单")
+    @Log(title = "供水异常设备派单", businessType = BusinessType.INSERT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:device:maintain')")
+    public AjaxResult createAbnormalOrder(@RequestBody MaintainOrderVO vo) {
+        try {
+            Long receiveUserId = vo == null ? null : vo.getReceiveUserId();
+            WorkOrder order = waterSupplyDeviceService.createAbnormalWorkOrder(vo);
+            if (order == null || order.getOrderId() == null) {
+                return AjaxResult.error("派单失败:工单创建未返回有效工单ID");
+            }
+            if (receiveUserId == null) {
+                order.setOrderStatus(1);
+                return AjaxResult.success("工单创建成功,待派单", order);
+            }
+            Long operatorId = SecurityUtils.getUserId();
+            String operatorName = SecurityUtils.getUsername();
+            boolean dispatched = businessWorkOrderService.dispatchOrder(order.getOrderId(), receiveUserId,
+                    vo.getMaintainRemark(), operatorId, operatorName);
+            if (!dispatched) {
+                return AjaxResult.error("派单失败:工单仍为待派单状态,请稍后重试");
+            }
+            order.setReceiveUser(String.valueOf(receiveUserId));
+            order.setOrderStatus(2);
+            return AjaxResult.success("派单成功", order);
+        } catch (ServiceException e) {
+            log.warn("供水异常设备派单业务失败: {}", e.getMessage());
+            return AjaxResult.error("派单失败:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("供水异常设备派单系统异常", e);
+            return AjaxResult.error("派单失败:服务暂不可用");
+        }
+    }
+
     @PostMapping("/export")
     @ApiOperation("导出异常设备台账")
     @Log(title = "导出异常设备", businessType = BusinessType.EXPORT)
     @PreAuthorize("@ss.hasPermi('waterSupply:device:abnormal:export')")
-    public AjaxResult exportAbnormalDevices(
+    public void exportAbnormalDevices(HttpServletResponse response,
             @ApiParam("设备名称") @RequestParam(required = false) String equipmentName,
             @ApiParam("设备编码") @RequestParam(required = false) String equipmentCode,
             @ApiParam("设备类型ID") @RequestParam(required = false) String equipmentTypeId,
             @ApiParam("告警等级") @RequestParam(required = false) Integer alertLevel) {
-        log.info("导出异常设备:name={}, code={}, typeId={}, level={}",
-                equipmentName, equipmentCode, equipmentTypeId, alertLevel);
-        return AjaxResult.success("导出功能待对接EasyExcel");
+        List<WaterDeviceAbnormalExportDTO> rows = waterSupplyDeviceService.getAbnormalDeviceExport(
+                        equipmentName, equipmentCode, equipmentTypeId, alertLevel)
+                .stream()
+                .map(WaterDeviceAbnormalExportDTO::from)
+                .collect(Collectors.toList());
+        new ExcelUtil<>(WaterDeviceAbnormalExportDTO.class).exportExcel(response, rows, "供水异常设备台账");
     }
 }

+ 19 - 7
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceLedger/DeviceLedgerController.java

@@ -4,9 +4,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zksy.base.domain.vo.DeviceGisVO;
 import com.zksy.base.domain.vo.DeviceTypeStatisticsVO;
 import com.zksy.base.service.WaterSupplyDeviceService;
+import com.zksy.WaterSupply.export.WaterDeviceLedgerExportDTO;
 import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.enums.BusinessType;
+import com.zksy.common.utils.poi.ExcelUtil;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -15,7 +17,9 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletResponse;
 import java.util.List;
+import java.util.stream.Collectors;
 
 @Slf4j
 @RestController
@@ -43,9 +47,12 @@ public class DeviceLedgerController {
             @ApiParam("设备名称") @RequestParam(required = false) String equipmentName,
             @ApiParam("设备编码") @RequestParam(required = false) String equipmentCode,
             @ApiParam("设备类型ID") @RequestParam(required = false) String equipmentTypeId,
-            @ApiParam("设备状态") @RequestParam(required = false) Integer status) {
+            @ApiParam("设备状态") @RequestParam(required = false) Integer status,
+            @ApiParam("报警状态") @RequestParam(required = false) Integer alarmStatus,
+            @ApiParam("报警等级") @RequestParam(required = false) Integer alertLevel) {
         Page<DeviceGisVO> page = waterSupplyDeviceService.getDeviceLedgerPage(
-                pageNum, pageSize, equipmentName, equipmentCode, equipmentTypeId, status);
+                pageNum, pageSize, equipmentName, equipmentCode, equipmentTypeId,
+                status, alarmStatus, alertLevel);
         return AjaxResult.success(page);
     }
 
@@ -62,13 +69,18 @@ public class DeviceLedgerController {
     @ApiOperation("批量导出设备台账")
     @Log(title = "导出设备台账", businessType = BusinessType.EXPORT)
     @PreAuthorize("@ss.hasPermi('waterSupply:device:export')")
-    public AjaxResult exportDeviceLedger(
+    public void exportDeviceLedger(HttpServletResponse response,
             @ApiParam("设备名称") @RequestParam(required = false) String equipmentName,
             @ApiParam("设备编码") @RequestParam(required = false) String equipmentCode,
             @ApiParam("设备类型ID") @RequestParam(required = false) String equipmentTypeId,
-            @ApiParam("设备状态") @RequestParam(required = false) Integer status) {
-        log.info("导出设备台账:name={}, code={}, typeId={}, status={}",
-                equipmentName, equipmentCode, equipmentTypeId, status);
-        return AjaxResult.success("导出功能待对接EasyExcel");
+            @ApiParam("设备状态") @RequestParam(required = false) Integer status,
+            @ApiParam("报警状态") @RequestParam(required = false) Integer alarmStatus,
+            @ApiParam("报警等级") @RequestParam(required = false) Integer alertLevel) {
+        List<WaterDeviceLedgerExportDTO> rows = waterSupplyDeviceService.getDeviceLedgerExport(
+                        equipmentName, equipmentCode, equipmentTypeId, status, alarmStatus, alertLevel)
+                .stream()
+                .map(WaterDeviceLedgerExportDTO::from)
+                .collect(Collectors.toList());
+        new ExcelUtil<>(WaterDeviceLedgerExportDTO.class).exportExcel(response, rows, "供水设备台账");
     }
 }

+ 29 - 4
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceMaintain/DeviceMaintainController.java

@@ -3,11 +3,14 @@ package com.zksy.web.controller.WaterSupply.DeviceMaintain;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zksy.base.domain.vo.EquipmentMaintainDataVO;
 import com.zksy.base.domain.vo.MaintainOrderVO;
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.service.BusinessWorkOrderService;
 import com.zksy.base.service.WaterSupplyDeviceService;
 import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.enums.BusinessType;
 import com.zksy.common.utils.SecurityUtils;
+import com.zksy.common.exception.ServiceException;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -28,17 +31,39 @@ public class DeviceMaintainController {
     @Autowired
     private WaterSupplyDeviceService waterSupplyDeviceService;
 
+    @Autowired
+    private BusinessWorkOrderService businessWorkOrderService;
+
     @PostMapping("/order")
     @ApiOperation("创建维修派单")
     @Log(title = "设备维修派单", businessType = BusinessType.INSERT)
     @PreAuthorize("@ss.hasPermi('waterSupply:device:maintain')")
     public AjaxResult createMaintainOrder(@RequestBody MaintainOrderVO vo) {
         try {
-            boolean result = waterSupplyDeviceService.createMaintainOrder(vo);
-            return result ? AjaxResult.success("派单成功") : AjaxResult.error("派单失败");
-        } catch (Exception e) {
-            log.error("创建维修派单失败", e);
+            Long receiveUserId = vo == null ? null : vo.getReceiveUserId();
+            WorkOrder order = waterSupplyDeviceService.createAbnormalWorkOrder(vo);
+            if (order == null || order.getOrderId() == null) {
+                return AjaxResult.error("派单失败:工单创建未返回有效工单ID");
+            }
+            if (receiveUserId == null) {
+                order.setOrderStatus(1);
+                return AjaxResult.success("工单创建成功,待派单", order);
+            }
+            boolean dispatched = businessWorkOrderService.dispatchOrder(order.getOrderId(),
+                    receiveUserId, vo.getMaintainRemark(), SecurityUtils.getUserId(),
+                    SecurityUtils.getUsername());
+            if (!dispatched) {
+                return AjaxResult.error("派单失败:工单仍为待派单状态,请稍后重试");
+            }
+            order.setReceiveUser(String.valueOf(receiveUserId));
+            order.setOrderStatus(2);
+            return AjaxResult.success("派单成功", order);
+        } catch (ServiceException e) {
+            log.warn("创建维修派单业务失败: {}", e.getMessage());
             return AjaxResult.error("派单失败:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("创建维修派单系统异常", e);
+            return AjaxResult.error("派单失败:服务暂不可用");
         }
     }
 

+ 156 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/DeviceAbnormal/DeviceAbnormalControllerTest.java

@@ -0,0 +1,156 @@
+package com.zksy.web.controller.WaterSupply.DeviceAbnormal;
+
+import com.zksy.base.domain.vo.AbnormalDeviceVO;
+import com.zksy.base.service.WaterSupplyDeviceService;
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.domain.vo.MaintainOrderVO;
+import com.zksy.base.service.BusinessWorkOrderService;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.common.core.domain.model.LoginUser;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class DeviceAbnormalControllerTest {
+
+    private WaterSupplyDeviceService deviceService;
+    private BusinessWorkOrderService businessWorkOrderService;
+    private MockMvc mockMvc;
+
+    @BeforeEach
+    void setUp() {
+        deviceService = mock(WaterSupplyDeviceService.class);
+        businessWorkOrderService = mock(BusinessWorkOrderService.class);
+        DeviceAbnormalController controller = new DeviceAbnormalController();
+        ReflectionTestUtils.setField(controller, "waterSupplyDeviceService", deviceService);
+        ReflectionTestUtils.setField(controller, "businessWorkOrderService", businessWorkOrderService);
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+        SysUser user = new SysUser();
+        user.setUserId(7L);
+        user.setUserName("operator");
+        LoginUser loginUser = new LoginUser(7L, 1L, user, java.util.Collections.emptySet());
+        SecurityContextHolder.getContext().setAuthentication(
+                new UsernamePasswordAuthenticationToken(loginUser, null, java.util.Collections.emptyList()));
+    }
+
+    @Test
+    void abnormalExportWritesExcelResponse() throws Exception {
+        AbnormalDeviceVO row = new AbnormalDeviceVO();
+        row.setEquipmentId("water-pressure-1");
+        row.setEquipmentName("供水压力监测点");
+        row.setAbnormalReason("供水压力超限");
+        when(deviceService.getAbnormalDeviceExport("供水", null, null, 1))
+                .thenReturn(Collections.singletonList(row));
+
+        mockMvc.perform(post("/waterSupply/device/abnormal/export")
+                        .param("equipmentName", "供水")
+                        .param("alertLevel", "1"))
+                .andExpect(status().isOk())
+                .andExpect(header().string("Content-Type",
+                        org.hamcrest.Matchers.containsString("spreadsheetml.sheet")))
+                .andExpect(result -> assertTrue(
+                        result.getResponse().getContentAsByteArray().length > 0));
+    }
+
+    @Test
+    void abnormalOrderReturnsUnifiedWorkOrderSummary() throws Exception {
+        WorkOrder order = new WorkOrder();
+        order.setOrderId(12L);
+        order.setOrderNo("WO-T6-001");
+        order.setOrderStatus(1);
+        when(deviceService.createAbnormalWorkOrder(any())).thenReturn(order);
+        when(businessWorkOrderService.dispatchOrder(12L, 88L, null, 7L, "operator")).thenReturn(true);
+
+        mockMvc.perform(post("/waterSupply/device/abnormal/order")
+                        .contentType("application/json")
+                        .content("{\"equipmentId\":\"water-1\",\"faultDescription\":\"压力异常\",\"receiveUserId\":88}"))
+                .andExpect(status().isOk())
+                .andExpect(result -> {
+                    String body = result.getResponse().getContentAsString();
+                    assertTrue(body.contains("WO-T6-001"));
+                    assertTrue(body.contains("orderId"));
+                    assertTrue(body.contains("orderStatus"));
+                    assertTrue(body.contains("\"orderStatus\":2"));
+                    assertTrue(body.contains("\"receiveUser\":\"88\""));
+                });
+        verify(businessWorkOrderService).dispatchOrder(12L, 88L, null, 7L, "operator");
+    }
+
+    @Test
+    void abnormalOrderWithoutReceiverCreatesPendingOrderWithoutDispatch() {
+        WorkOrder order = new WorkOrder();
+        order.setOrderId(14L);
+        order.setOrderNo("WO-T6-PENDING");
+        order.setOrderStatus(1);
+        when(deviceService.createAbnormalWorkOrder(any())).thenReturn(order);
+
+        MaintainOrderVO request = new MaintainOrderVO();
+        request.setEquipmentId("water-1");
+        request.setFaultDescription("压力异常");
+        AjaxResult result = new DeviceAbnormalController() {{
+            ReflectionTestUtils.setField(this, "waterSupplyDeviceService", deviceService);
+            ReflectionTestUtils.setField(this, "businessWorkOrderService", businessWorkOrderService);
+        }}.createAbnormalOrder(request);
+
+        assertEquals(200, result.get("code"));
+        assertEquals(1, ((WorkOrder) result.get("data")).getOrderStatus());
+        verify(businessWorkOrderService, org.mockito.Mockito.never())
+                .dispatchOrder(org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
+                        org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
+    }
+
+    @Test
+    void abnormalOrderHidesUnexpectedExceptionDetails() {
+        DeviceAbnormalController controller = new DeviceAbnormalController();
+        ReflectionTestUtils.setField(controller, "waterSupplyDeviceService", deviceService);
+        ReflectionTestUtils.setField(controller, "businessWorkOrderService", businessWorkOrderService);
+        when(deviceService.createAbnormalWorkOrder(any())).thenThrow(new IllegalStateException("database password"));
+        MaintainOrderVO request = new MaintainOrderVO();
+        request.setEquipmentId("water-1");
+        request.setReceiveUserId(88L);
+        AjaxResult result = controller.createAbnormalOrder(request);
+        assertEquals("派单失败:服务暂不可用", String.valueOf(result.get("msg")));
+        assertTrue(!String.valueOf(result.get("msg")).contains("database password"));
+    }
+
+    @Test
+    void abnormalOrderReportsDispatchFailureAndKeepsPendingStatus() throws Exception {
+        WorkOrder order = new WorkOrder();
+        order.setOrderId(13L);
+        order.setOrderNo("WO-T6-002");
+        order.setOrderStatus(1);
+        when(deviceService.createAbnormalWorkOrder(any())).thenReturn(order);
+        when(businessWorkOrderService.dispatchOrder(13L, 88L, null, 7L, "operator")).thenReturn(false);
+
+        mockMvc.perform(post("/waterSupply/device/abnormal/order")
+                        .contentType("application/json")
+                        .content("{\"equipmentId\":\"water-1\",\"faultDescription\":\"压力异常\",\"receiveUserId\":88}"))
+                .andExpect(status().isOk())
+                .andExpect(result -> {
+                    JsonNode json = new ObjectMapper().readTree(result.getResponse().getContentAsString());
+                    assertEquals(500, json.get("code").asInt());
+                    assertTrue(json.hasNonNull("msg"));
+                    assertTrue(!json.get("msg").asText().isEmpty());
+                });
+        assertEquals(1, order.getOrderStatus());
+    }
+}

+ 50 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/DeviceLedger/DeviceLedgerControllerTest.java

@@ -0,0 +1,50 @@
+package com.zksy.web.controller.WaterSupply.DeviceLedger;
+
+import com.zksy.base.domain.vo.DeviceGisVO;
+import com.zksy.base.service.WaterSupplyDeviceService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class DeviceLedgerControllerTest {
+
+    private WaterSupplyDeviceService deviceService;
+    private MockMvc mockMvc;
+
+    @BeforeEach
+    void setUp() {
+        deviceService = mock(WaterSupplyDeviceService.class);
+        DeviceLedgerController controller = new DeviceLedgerController();
+        ReflectionTestUtils.setField(controller, "waterSupplyDeviceService", deviceService);
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    @Test
+    void ledgerExportWritesExcelResponse() throws Exception {
+        DeviceGisVO row = new DeviceGisVO();
+        row.setEquipmentId("water-pressure-1");
+        row.setEquipmentName("供水压力监测点");
+        when(deviceService.getDeviceLedgerExport("供水", null, null, 1, null, null))
+                .thenReturn(Collections.singletonList(row));
+
+        mockMvc.perform(post("/waterSupply/device/ledger/export")
+                        .param("equipmentName", "供水")
+                        .param("status", "1"))
+                .andExpect(status().isOk())
+                .andExpect(header().string("Content-Type",
+                        org.hamcrest.Matchers.containsString("spreadsheetml.sheet")))
+                .andExpect(result -> assertTrue(
+                        result.getResponse().getContentAsByteArray().length > 0));
+    }
+}

+ 76 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterDeviceAbnormalExportDTO.java

@@ -0,0 +1,76 @@
+package com.zksy.WaterSupply.export;
+
+import com.zksy.base.domain.vo.AbnormalDeviceVO;
+import com.zksy.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+public class WaterDeviceAbnormalExportDTO {
+    @Excel(name = "设备编码")
+    private String equipmentCode;
+
+    @Excel(name = "设备名称")
+    private String equipmentName;
+
+    @Excel(name = "设备类型")
+    private String equipmentTypeName;
+
+    @Excel(name = "安装位置")
+    private String equipmentLocation;
+
+    @Excel(name = "设备状态", readConverterExp = "1=在用,2=闲置,3=维修,4=报废,5=待入库")
+    private Integer currentStatus;
+
+    @Excel(name = "在线状态", readConverterExp = "0=离线,1=在线")
+    private Integer onlineStatus;
+
+    @Excel(name = "报警状态", readConverterExp = "0=正常,1=报警")
+    private Integer alarmStatus;
+
+    @Excel(name = "报警等级", readConverterExp = "1=一级,2=二级,3=三级,4=四级")
+    private Integer alertLevel;
+
+    @Excel(name = "异常原因")
+    private String abnormalReason;
+
+    @Excel(name = "关联工单号")
+    private String relatedWorkOrderNo;
+
+    @Excel(name = "工单状态", readConverterExp = "1=待派单,2=待接单,3=已接单,4=处理中,5=待验收,6=已结案,7=已驳回,8=延期审核,9=已延期")
+    private Integer relatedWorkOrderStatus;
+
+    @Excel(name = "接单责任人")
+    private String relatedWorkOrderPerson;
+
+    @Excel(name = "经度")
+    private BigDecimal longitude;
+
+    @Excel(name = "纬度")
+    private BigDecimal latitude;
+
+    @Excel(name = "状态更新时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime statusUpdateTime;
+
+    public static WaterDeviceAbnormalExportDTO from(AbnormalDeviceVO value) {
+        WaterDeviceAbnormalExportDTO dto = new WaterDeviceAbnormalExportDTO();
+        dto.equipmentCode = value.getEquipmentCode();
+        dto.equipmentName = value.getEquipmentName();
+        dto.equipmentTypeName = value.getEquipmentTypeName();
+        dto.equipmentLocation = value.getEquipmentLocation();
+        dto.currentStatus = value.getCurrentStatus();
+        dto.onlineStatus = value.getOnlineStatus();
+        dto.alarmStatus = value.getAlarmStatus();
+        dto.alertLevel = value.getAlertLevel();
+        dto.abnormalReason = value.getAbnormalReason();
+        dto.relatedWorkOrderNo = value.getRelatedWorkOrderNo();
+        dto.relatedWorkOrderStatus = value.getRelatedWorkOrderStatus();
+        dto.relatedWorkOrderPerson = value.getRelatedWorkOrderPerson();
+        dto.longitude = value.getLongitude();
+        dto.latitude = value.getLatitude();
+        dto.statusUpdateTime = value.getStatusUpdateTime();
+        return dto;
+    }
+}

+ 51 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterDeviceLedgerExportDTO.java

@@ -0,0 +1,51 @@
+package com.zksy.WaterSupply.export;
+
+import com.zksy.base.domain.vo.DeviceGisVO;
+import com.zksy.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class WaterDeviceLedgerExportDTO {
+    @Excel(name = "设备编码")
+    private String equipmentCode;
+
+    @Excel(name = "设备名称")
+    private String equipmentName;
+
+    @Excel(name = "设备类型")
+    private String equipmentTypeName;
+
+    @Excel(name = "安装位置")
+    private String equipmentLocation;
+
+    @Excel(name = "设备状态", readConverterExp = "1=在用,2=闲置,3=维修,4=报废,5=待入库")
+    private Integer currentStatus;
+
+    @Excel(name = "在线状态", readConverterExp = "0=离线,1=在线")
+    private Integer onlineStatus;
+
+    @Excel(name = "报警状态", readConverterExp = "0=正常,1=报警")
+    private Integer alarmStatus;
+
+    @Excel(name = "经度")
+    private BigDecimal longitude;
+
+    @Excel(name = "纬度")
+    private BigDecimal latitude;
+
+    public static WaterDeviceLedgerExportDTO from(DeviceGisVO value) {
+        WaterDeviceLedgerExportDTO dto = new WaterDeviceLedgerExportDTO();
+        dto.equipmentCode = value.getEquipmentCode();
+        dto.equipmentName = value.getEquipmentName();
+        dto.equipmentTypeName = value.getEquipmentTypeName();
+        dto.equipmentLocation = value.getEquipmentLocation();
+        dto.currentStatus = value.getCurrentStatus();
+        dto.onlineStatus = value.getOnlineStatus();
+        dto.alarmStatus = value.getAlarmStatus();
+        dto.longitude = value.getLongitude();
+        dto.latitude = value.getLatitude();
+        return dto;
+    }
+}

+ 15 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/AbnormalDeviceVO.java

@@ -53,6 +53,21 @@ public class AbnormalDeviceVO {
     @ApiModelProperty("告警等级 1-4,1级最高")
     private Integer alertLevel;
 
+    @ApiModelProperty("异常原因(来自未处理报警或维修状态)")
+    private String abnormalReason;
+
+    @ApiModelProperty("报警时间")
+    private LocalDateTime alarmTime;
+
+    @ApiModelProperty("关联统一工单号")
+    private String relatedWorkOrderNo;
+
+    @ApiModelProperty("关联统一工单状态:1-待派单 2-待接单 3-已接单 4-处理中 5-待验收 6-已结案 7-已驳回 8-延期审核 9-已延期")
+    private Integer relatedWorkOrderStatus;
+
+    @ApiModelProperty("关联统一工单运维人员")
+    private String relatedWorkOrderPerson;
+
     @ApiModelProperty("最新工单号")
     private String latestMaintainId;
 

+ 15 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/DeviceDetailVO.java

@@ -69,6 +69,21 @@ public class DeviceDetailVO {
     @ApiModelProperty("当前状态")
     private Integer currentStatus;
 
+    @ApiModelProperty("告警等级 1-4,1级最高")
+    private Integer alertLevel;
+
+    @ApiModelProperty("异常原因")
+    private String abnormalReason;
+
+    @ApiModelProperty("关联统一工单号")
+    private String relatedWorkOrderNo;
+
+    @ApiModelProperty("关联统一工单状态")
+    private Integer relatedWorkOrderStatus;
+
+    @ApiModelProperty("关联统一工单责任人")
+    private String relatedWorkOrderPerson;
+
     @ApiModelProperty("最新监测数据(动态字段)")
     private Map<String, Object> latestData;
 

+ 6 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/MaintainOrderVO.java

@@ -26,6 +26,12 @@ public class MaintainOrderVO {
     @ApiModelProperty("维护内容/故障描述")
     private String maintainContent;
 
+    @ApiModelProperty("故障描述(兼容字段)")
+    private String faultDescription;
+
+    @ApiModelProperty("单一接单责任人用户ID")
+    private Long receiveUserId;
+
     @ApiModelProperty("维护人")
     private String maintainPerson;
 

+ 17 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/WaterSupplyDeviceService.java

@@ -3,6 +3,7 @@ package com.zksy.base.service;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.WorkOrder;
 import com.zksy.base.domain.vo.*;
 
 import java.util.List;
@@ -14,7 +15,12 @@ public interface WaterSupplyDeviceService extends IService<EquipmentBase> {
 
     Page<DeviceGisVO> getDeviceLedgerPage(long pageNum, long pageSize,
                                            String equipmentName, String equipmentCode,
-                                           String equipmentTypeId, Integer status);
+                                           String equipmentTypeId, Integer status,
+                                           Integer alarmStatus, Integer alertLevel);
+
+    List<DeviceGisVO> getDeviceLedgerExport(String equipmentName, String equipmentCode,
+                                             String equipmentTypeId, Integer status,
+                                             Integer alarmStatus, Integer alertLevel);
 
     List<DeviceGisVO> getAllDeviceGisPoints(String equipmentTypeId);
 
@@ -22,10 +28,20 @@ public interface WaterSupplyDeviceService extends IService<EquipmentBase> {
 
     boolean createMaintainOrder(MaintainOrderVO vo);
 
+    WorkOrder createAbnormalWorkOrder(MaintainOrderVO vo);
+
     Page<AbnormalDeviceVO> getAbnormalDevicePage(long pageNum, long pageSize,
                                                   String equipmentName, String equipmentCode,
                                                   String equipmentTypeId, Integer alertLevel);
 
+    Page<AbnormalDeviceVO> getAbnormalDevicePage(long pageNum, long pageSize,
+                                                  String equipmentName, String equipmentCode,
+                                                  String equipmentTypeId, Integer alertLevel,
+                                                  Boolean dispatchableOnly);
+
+    List<AbnormalDeviceVO> getAbnormalDeviceExport(String equipmentName, String equipmentCode,
+                                                    String equipmentTypeId, Integer alertLevel);
+
     List<AbnormalDeviceVO> getAbnormalDeviceGisPoints(String equipmentTypeId);
 
     List<Map<String, Object>> getMaintainRecordList(String equipmentId);

+ 38 - 9
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/BusinessWorkOrderServiceImpl.java

@@ -14,7 +14,9 @@ import com.zksy.base.mapper.HazardHiddenAccountMapper;
 import com.zksy.base.mapper.WorkOrderLogMapper;
 import com.zksy.base.mapper.WorkOrderMapper;
 import com.zksy.base.service.BusinessWorkOrderService;
+import com.zksy.common.core.domain.entity.SysUser;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.system.service.ISysUserService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -43,6 +45,9 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
     @Autowired
     private HazardHiddenAccountMapper hazardHiddenAccountMapper;
 
+    @Autowired
+    private ISysUserService sysUserService;
+
     @Override
     public IPage<WorkOrder> selectWorkOrderList(Integer orderType, Integer orderLevel, String orderStatus, String deviceCode, String keyword, String equipmentType, Integer pageNum, Integer pageSize) {
         Page<WorkOrder> page = new Page<>(pageNum, pageSize);
@@ -102,6 +107,7 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean dispatchOrder(Long orderId, Long handlerId, String remarks, Long operUserId, String operUserName) {
+        validateDispatchHandler(handlerId);
         WorkOrder order = this.getById(orderId);
         if (order == null) throw new ServiceException("工单不存在");
         if (order.getOrderStatus() != 1) throw new ServiceException("当前工单状态不允许派单");
@@ -131,6 +137,7 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
         if (handlerIds == null || handlerIds.isEmpty()) {
             throw new ServiceException("派单人员不能为空");
         }
+        handlerIds.forEach(this::validateDispatchHandler);
         WorkOrder order = this.getById(orderId);
         if (order == null) throw new ServiceException("工单不存在");
         if (order.getOrderStatus() != 1) throw new ServiceException("当前工单状态不允许派单");
@@ -162,6 +169,20 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
         return true;
     }
 
+    private void validateDispatchHandler(Long handlerId) {
+        if (handlerId == null) {
+            throw new ServiceException("派单人员不能为空");
+        }
+        if (sysUserService == null) {
+            throw new ServiceException("责任人校验服务不可用");
+        }
+        SysUser user = sysUserService.selectUserById(handlerId);
+        if (user == null || !"0".equals(String.valueOf(user.getStatus()))
+                || !"0".equals(String.valueOf(user.getDelFlag()))) {
+            throw new ServiceException("接单责任人不存在或已停用");
+        }
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean verifyOrder(Long orderId, String handleResult, String solution, Long operUserId, String operUserName) {
@@ -248,7 +269,7 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
     @Transactional(rollbackFor = Exception.class)
     public Long createWorkOrderFromHazard(String hazardId, String hazardNo, String hazardDesc, String equipmentId, String hazardLevel, String hazardImages, Date planFinishTime) {
         // 同一设备存在进行中工单(状态非 6 已结案/7 已驳回)时禁止重复建单
-        checkDeviceOrderConflict(equipmentId);
+        checkDeviceOrderConflict(equipmentId, 1, null);
 
         // 隐患等级映射到工单优先级:0-重大→紧急(1) 1-较大→一般(2) 2-一般→一般(2) 3-轻微→低(3)
         Integer orderLevel;
@@ -300,11 +321,10 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
     @Transactional(rollbackFor = Exception.class)
     public Long createWorkOrder(WorkOrder workOrder) {
         // 同一设备存在进行中工单(状态非 6 已结案/7 已驳回)时禁止重复建单
-        checkDeviceOrderConflict(workOrder.getDeviceId());
-
         if (workOrder.getOrderType() == null) {
             workOrder.setOrderType(1);
         }
+        checkDeviceOrderConflict(workOrder.getDeviceId(), workOrder.getOrderType(), workOrder.getAlarmId());
         if (workOrder.getOrderLevel() == null) {
             workOrder.setOrderLevel(2);
         }
@@ -378,17 +398,26 @@ public class BusinessWorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, W
     }
 
     /**
-     * 校验设备是否存在进行中工单(状态 1-5/8/9),存在则禁止再次创建工单
-     * 业务规则:同一设备同一时间只能有一个进行中的工单,结案(6)或驳回(7)后方可再次创建
+     * 校验同一业务对象是否已经存在进行中的同类工单。
+     * 带报警身份的工单按设备、工单类型和报警记录去重;没有报警身份的
+     * 手工工单只与同设备的另一条未关联报警工单冲突,避免挡住不同报警原因。
      */
-    private void checkDeviceOrderConflict(String deviceId) {
+    private void checkDeviceOrderConflict(String deviceId, Integer orderType, String alarmId) {
         if (StringUtils.isEmpty(deviceId)) {
             return;
         }
-        Long count = this.lambdaQuery()
+        com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper<WorkOrder> query = this.lambdaQuery()
                 .eq(WorkOrder::getDeviceId, deviceId)
-                .notIn(WorkOrder::getOrderStatus, 6, 7)
-                .count();
+                .notIn(WorkOrder::getOrderStatus, 6, 7);
+        if (orderType != null) {
+            query.eq(WorkOrder::getOrderType, orderType);
+        }
+        if (StringUtils.isNotBlank(alarmId)) {
+            query.eq(WorkOrder::getAlarmId, alarmId);
+        } else {
+            query.isNull(WorkOrder::getAlarmId);
+        }
+        Long count = query.count();
         if (count > 0) {
             throw new ServiceException("该设备已有处理中的工单,需结案或驳回后才能创建新工单");
         }

+ 514 - 217
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WaterSupplyDeviceServiceImpl.java

@@ -6,6 +6,7 @@ import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.alarm.mapper.AlarmDataMapper;
 import com.zksy.base.audio.domain.NoiseInfo;
 import com.zksy.base.audio.mapper.NoiseInfoMapper;
@@ -13,6 +14,7 @@ import com.zksy.base.domain.EquipmentBase;
 import com.zksy.base.domain.EquipmentMaintain;
 import com.zksy.base.domain.EquipmentStatus;
 import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.domain.WorkOrder;
 import com.zksy.base.domain.vo.*;
 import com.zksy.base.event.DeviceStatusChangePublisher;
 import com.zksy.base.environment.domain.ERealTimeData;
@@ -23,18 +25,24 @@ import com.zksy.base.mapper.EquipmentBaseMapper;
 import com.zksy.base.mapper.EquipmentMaintainMapper;
 import com.zksy.base.mapper.EquipmentStatusMapper;
 import com.zksy.base.mapper.EquipmentTypeMapper;
+import com.zksy.base.mapper.WorkOrderMapper;
 import com.zksy.base.pressure.domain.FirefightingPressure;
 import com.zksy.base.pressure.mapper.FirefightingPressureMapper;
 import com.zksy.base.radar.domain.RadarData;
 import com.zksy.base.radar.mapper.RadarDataMapper;
+import com.zksy.base.service.EquipmentBaseService;
 import com.zksy.base.service.WaterSupplyDeviceService;
 import com.zksy.base.telemetry.domain.TelemetryData;
 import com.zksy.base.telemetry.mapper.TelemetryDataMapper;
 import com.zksy.base.water.domain.WaterMonitorData;
 import com.zksy.base.water.mapper.WaterMonitorDataMapper;
 import com.zksy.common.sms.SmsTemplate;
+import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.common.utils.SecurityUtils;
+import com.zksy.system.service.ISysUserService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -84,44 +92,69 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
     @Autowired
     private AlarmDataMapper alarmDataMapper;
 
+    @Autowired
+    private EquipmentBaseService equipmentBaseService;
+
+    @Autowired
+    private WorkOrderMapper workOrderMapper;
+
+    @Autowired
+    private com.zksy.base.service.BusinessWorkOrderService businessWorkOrderService;
+
+    @Autowired
+    private ISysUserService sysUserService;
+
+    @Value("${waterSupply.device.top-level-type:供水}")
+    private String waterTopLevelTypeName = "供水";
+
     @Autowired(required = false)
     private SmsTemplate smsTemplate;
 
     @Override
     public List<DeviceTypeStatisticsVO> getDeviceTypeStatistics() {
+        List<EquipmentBase> waterDevices = findWaterDevices();
+        if (CollUtil.isEmpty(waterDevices)) {
+            return new ArrayList<>();
+        }
+
+        Map<String, Long> typeCountMap = waterDevices.stream()
+                .filter(d -> d.getEquipmentTypeId() != null)
+                .collect(Collectors.groupingBy(EquipmentBase::getEquipmentTypeId, Collectors.counting()));
+
         List<EquipmentType> allTypes = equipmentTypeMapper.selectList(
                 new LambdaQueryWrapper<EquipmentType>().orderByAsc(EquipmentType::getTypeId)
         );
         if (CollUtil.isEmpty(allTypes)) {
             return new ArrayList<>();
         }
-
-        List<EquipmentBase> allDevices = this.list(
-                new LambdaQueryWrapper<EquipmentBase>()
-                        .select(EquipmentBase::getEquipmentId, EquipmentBase::getEquipmentTypeId)
-        );
-        Map<String, Long> typeCountMap = allDevices.stream()
-                .filter(d -> d.getEquipmentTypeId() != null)
-                .collect(Collectors.groupingBy(EquipmentBase::getEquipmentTypeId, Collectors.counting()));
+        Set<String> waterTypeRecordIds = resolveOwnedTypeRecordIds(allTypes, typeCountMap.keySet());
 
         Map<String, DeviceTypeStatisticsVO> voMap = new HashMap<>();
         for (EquipmentType type : allTypes) {
+            if (!waterTypeRecordIds.contains(type.getId())) {
+                continue;
+            }
             DeviceTypeStatisticsVO vo = new DeviceTypeStatisticsVO();
             vo.setTypeId(type.getTypeId());
             vo.setTypeName(type.getTypeName());
             vo.setParentTypeId(type.getParentTypeId());
-            vo.setDeviceCount(typeCountMap.getOrDefault(type.getTypeId(), 0L).intValue());
+            vo.setDeviceCount(typeCountMap.getOrDefault(type.getId(), 0L).intValue());
             vo.setChildren(new ArrayList<>());
-            voMap.put(type.getTypeId(), vo);
+            voMap.put(type.getId(), vo);
         }
 
         List<DeviceTypeStatisticsVO> roots = new ArrayList<>();
-        for (DeviceTypeStatisticsVO vo : voMap.values()) {
-            String parentId = vo.getParentTypeId();
-            if (parentId == null || "0".equals(parentId) || !voMap.containsKey(parentId)) {
+        for (EquipmentType type : allTypes) {
+            DeviceTypeStatisticsVO vo = voMap.get(type.getId());
+            if (vo == null) {
+                continue;
+            }
+            String parentRecordId = type.getParentTypeId();
+            if (parentRecordId == null || "0".equals(parentRecordId)
+                    || !voMap.containsKey(parentRecordId)) {
                 roots.add(vo);
             } else {
-                voMap.get(parentId).getChildren().add(vo);
+                voMap.get(parentRecordId).getChildren().add(vo);
             }
         }
 
@@ -145,25 +178,47 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
     @Override
     public Page<DeviceGisVO> getDeviceLedgerPage(long pageNum, long pageSize,
                                                   String equipmentName, String equipmentCode,
-                                                  String equipmentTypeId, Integer status) {
-        Page<EquipmentBase> page = new Page<>(pageNum, pageSize);
-        LambdaQueryWrapper<EquipmentBase> wrapper = new LambdaQueryWrapper<>();
-        wrapper.like(StrUtil.isNotBlank(equipmentName), EquipmentBase::getEquipmentName, equipmentName)
-                .like(StrUtil.isNotBlank(equipmentCode), EquipmentBase::getEquipmentCode, equipmentCode)
-                .eq(StrUtil.isNotBlank(equipmentTypeId), EquipmentBase::getEquipmentTypeId, equipmentTypeId)
-                .orderByDesc(EquipmentBase::getCreateTime);
-        Page<EquipmentBase> basePage = this.page(page, wrapper);
+                                                  String equipmentTypeId, Integer status,
+                                                  Integer alarmStatus, Integer alertLevel) {
+        List<EquipmentBase> waterDevices = findWaterDevices();
+        if (CollUtil.isEmpty(waterDevices)) {
+            return emptyPage(pageNum, pageSize);
+        }
 
+        Map<String, EquipmentStatus> statusMap = status != null || alarmStatus != null || alertLevel != null
+                ? getStatusMap(waterDevices.stream().map(EquipmentBase::getEquipmentId).collect(Collectors.toList()))
+                : new HashMap<>();
+        final Map<String, EquipmentStatus> filterStatusMap = statusMap;
+        Map<String, AlarmData> alarmMap = alertLevel != null ? getActiveAlarmMap(waterDevices) : new HashMap<>();
+
+        List<EquipmentBase> records = waterDevices.stream()
+                .filter(device -> matchesLedgerFilters(device, equipmentName, equipmentCode, equipmentTypeId))
+                .filter(device -> status == null || status.equals(Optional.ofNullable(filterStatusMap
+                        .get(device.getEquipmentId())).map(EquipmentStatus::getCurrentStatus).orElse(null)))
+                .filter(device -> alarmStatus == null || alarmStatus.equals(Optional.ofNullable(filterStatusMap
+                        .get(device.getEquipmentId())).map(EquipmentStatus::getAlarmStatus).orElse(null)))
+                .filter(device -> alertLevel == null || alertLevel.equals(resolveAlertLevel(device,
+                        filterStatusMap.get(device.getEquipmentId()), alarmMap)))
+                .sorted(Comparator.comparing(EquipmentBase::getCreateTime,
+                        Comparator.nullsLast(Comparator.reverseOrder())))
+                .collect(Collectors.toList());
+
+        Page<EquipmentBase> basePage = paginate(records, pageNum, pageSize);
         Page<DeviceGisVO> resultPage = new Page<>(pageNum, pageSize, basePage.getTotal());
         if (CollUtil.isEmpty(basePage.getRecords())) {
             resultPage.setRecords(new ArrayList<>());
             return resultPage;
         }
 
-        List<EquipmentBase> records = basePage.getRecords();
+        records = basePage.getRecords();
         List<String> equipmentIds = records.stream().map(EquipmentBase::getEquipmentId).collect(Collectors.toList());
 
-        Map<String, EquipmentStatus> statusMap = getStatusMap(equipmentIds);
+        if (status == null && alarmStatus == null && alertLevel == null) {
+            statusMap = getStatusMap(equipmentIds);
+        }
+        Map<String, AlarmData> displayAlarmMap = CollUtil.isEmpty(alarmMap)
+                ? getActiveAlarmMap(records)
+                : alarmMap;
         Map<String, EquipmentType> typeMap = getTypeMap(records);
 
         List<DeviceGisVO> voList = new ArrayList<>();
@@ -175,6 +230,10 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
                 vo.setAlarmStatus(es.getAlarmStatus());
                 vo.setCurrentStatus(es.getCurrentStatus());
             }
+            AlarmData alarm = displayAlarmMap.get(base.getEquipmentCode());
+            if (alarm != null) {
+                vo.setAlertLevel(alarm.getAlarmLevel());
+            }
             EquipmentType et = typeMap.get(base.getEquipmentTypeId());
             if (et != null) {
                 vo.setEquipmentTypeName(et.getTypeName());
@@ -186,13 +245,21 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
         return resultPage;
     }
 
+    @Override
+    public List<DeviceGisVO> getDeviceLedgerExport(String equipmentName, String equipmentCode,
+                                                    String equipmentTypeId, Integer status,
+                                                    Integer alarmStatus, Integer alertLevel) {
+        return getDeviceLedgerPage(1, Integer.MAX_VALUE, equipmentName, equipmentCode,
+                equipmentTypeId, status, alarmStatus, alertLevel).getRecords();
+    }
+
     @Override
     public List<DeviceGisVO> getAllDeviceGisPoints(String equipmentTypeId) {
-        LambdaQueryWrapper<EquipmentBase> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StrUtil.isNotBlank(equipmentTypeId), EquipmentBase::getEquipmentTypeId, equipmentTypeId)
-                .isNotNull(EquipmentBase::getLongitude)
-                .isNotNull(EquipmentBase::getLatitude);
-        List<EquipmentBase> list = this.list(wrapper);
+        List<EquipmentBase> list = findWaterDevices().stream()
+                .filter(device -> StrUtil.isBlank(equipmentTypeId)
+                        || equipmentTypeId.equals(device.getEquipmentTypeId()))
+                .filter(device -> device.getLongitude() != null && device.getLatitude() != null)
+                .collect(Collectors.toList());
         if (CollUtil.isEmpty(list)) {
             return new ArrayList<>();
         }
@@ -222,7 +289,7 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
 
     @Override
     public DeviceDetailVO getDeviceDetail(String equipmentId) {
-        EquipmentBase base = this.getById(equipmentId);
+        EquipmentBase base = findWaterDeviceById(equipmentId);
         if (base == null) {
             return null;
         }
@@ -242,6 +309,25 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
             vo.setCurrentStatus(status.getCurrentStatus());
         }
 
+        // alarm_data is historical evidence only; equipment_status is authoritative
+        // for whether the device is currently alarming.
+        AlarmData activeAlarm = status != null && Objects.equals(status.getAlarmStatus(), 1)
+                ? getActiveAlarmMap(Collections.singletonList(base)).get(base.getEquipmentCode())
+                : null;
+        if (activeAlarm != null) {
+            vo.setAlertLevel(activeAlarm.getAlarmLevel());
+            vo.setAbnormalReason(StrUtil.isNotBlank(activeAlarm.getWarningType()) ? activeAlarm.getWarningType() : "设备报警");
+        } else if (status != null && Objects.equals(status.getCurrentStatus(), 3)) {
+            // Maintenance is an operational status, not a monitoring alarm level.
+            vo.setAbnormalReason("设备维修中");
+        }
+        WorkOrder relatedOrder = getLatestWorkOrderMap(Collections.singletonList(base)).get(equipmentId);
+        if (relatedOrder != null) {
+            vo.setRelatedWorkOrderNo(relatedOrder.getOrderNo());
+            vo.setRelatedWorkOrderStatus(relatedOrder.getOrderStatus());
+            vo.setRelatedWorkOrderPerson(resolveWorkOrderPerson(relatedOrder));
+        }
+
         fillGpsFields(vo, base.getLongitude(), base.getLatitude());
 
         Map<String, Object> latestData = getLatestMonitorData(base.getEquipmentCode(), base.getEquipmentTypeId());
@@ -271,54 +357,56 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
 
     @Override
     @Transactional(rollbackFor = Exception.class)
+    @Deprecated
     public boolean createMaintainOrder(MaintainOrderVO vo) {
-        EquipmentBase device = this.getById(vo.getEquipmentId());
-        if (device == null) {
-            throw new RuntimeException("设备不存在:" + vo.getEquipmentId());
-        }
-
-        EquipmentMaintain maintain = new EquipmentMaintain();
-        maintain.setEquipmentId(vo.getEquipmentId());
-        maintain.setMaintainType(StrUtil.isNotBlank(vo.getMaintainType()) ? vo.getMaintainType() : "故障维修");
-        maintain.setMaintainContent(vo.getMaintainContent());
-        maintain.setMaintainPerson(vo.getMaintainPerson());
-        maintain.setMaintainPersonPhone(vo.getMaintainPersonPhone());
-        maintain.setMaintainCost(vo.getMaintainCost());
-        maintain.setMaintainRemark(vo.getMaintainRemark());
-        int insert = equipmentMaintainMapper.insert(maintain);
-
-        if (insert > 0) {
-            EquipmentStatus status = equipmentStatusMapper.selectOne(
-                    new LambdaQueryWrapper<EquipmentStatus>().eq(EquipmentStatus::getEquipmentId, vo.getEquipmentId())
-            );
-            if (status != null) {
-                boolean changed = !Objects.equals(status.getCurrentStatus(), 3);
-                status.setCurrentStatus(3);
-                status.setStatusUpdateTime(LocalDateTime.now());
-                boolean updated = equipmentStatusMapper.updateById(status) > 0;
-                if (updated && changed) {
-                    publishEquipmentStatusChange(device, status, "pipe-network:createMaintainOrder");
-                }
-            } else {
-                EquipmentStatus newStatus = new EquipmentStatus();
-                newStatus.setEquipmentId(vo.getEquipmentId());
-                newStatus.setCurrentStatus(3);
-                newStatus.setAlarmStatus(0);
-                newStatus.setOnlineStatus(1);
-                newStatus.setStatusUpdateTime(LocalDateTime.now());
-                newStatus.setCreateTime(LocalDateTime.now());
-                if (equipmentStatusMapper.insert(newStatus) > 0) {
-                    publishEquipmentStatusChange(device, newStatus, "pipe-network:createMaintainOrder");
-                }
-            }
-            log.info("设备[{}]派单成功,工单ID:{},维修人:{}",
-                    vo.getEquipmentId(), maintain.getMaintainId(), vo.getMaintainPerson());
+        // Keep the legacy service signature for binary compatibility, but route it
+        // through the same unified WorkOrder lifecycle as the current endpoint.
+        WorkOrder workOrder = createAbnormalWorkOrder(vo);
+        if (workOrder == null || workOrder.getOrderId() == null) {
+            return false;
+        }
+        if (vo.getReceiveUserId() == null) {
+            return true;
+        }
+        return businessWorkOrderService.dispatchOrder(workOrder.getOrderId(), vo.getReceiveUserId(),
+                vo.getMaintainRemark(), currentUserIdOrNull(), currentUserNameOrNull());
+    }
 
-            // 发送短信通知维修人员
-            sendMaintainSmsNotify(device, maintain);
+    private Long currentUserIdOrNull() {
+        try {
+            return SecurityUtils.getUserId();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private String currentUserNameOrNull() {
+        try {
+            return SecurityUtils.getUsername();
+        } catch (Exception e) {
+            return null;
         }
+    }
 
-        return insert > 0;
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public WorkOrder createAbnormalWorkOrder(MaintainOrderVO vo) {
+        if (vo == null || StrUtil.isBlank(vo.getEquipmentId())) {
+            throw new RuntimeException("设备ID不能为空");
+        }
+        EquipmentBase device = findWaterDeviceById(vo.getEquipmentId());
+        if (device == null) {
+            throw new RuntimeException("仅支持供水设备派发维修工单");
+        }
+        WorkOrder workOrder = new WorkOrder();
+        workOrder.setDeviceId(device.getEquipmentId());
+        workOrder.setDeviceCode(device.getEquipmentCode());
+        workOrder.setOrderType(1);
+        workOrder.setOrderLevel(2);
+        workOrder.setOrderDesc(StrUtil.isNotBlank(vo.getFaultDescription()) ? vo.getFaultDescription() : vo.getMaintainContent());
+        Long orderId = businessWorkOrderService.createWorkOrder(workOrder);
+        workOrder.setOrderId(orderId);
+        return workOrder;
     }
 
     private void publishEquipmentStatusChange(EquipmentBase equipment, EquipmentStatus status, String source) {
@@ -366,122 +454,117 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
     public Page<AbnormalDeviceVO> getAbnormalDevicePage(long pageNum, long pageSize,
                                                          String equipmentName, String equipmentCode,
                                                          String equipmentTypeId, Integer alertLevel) {
+        return getAbnormalDevicePage(pageNum, pageSize, equipmentName, equipmentCode,
+                equipmentTypeId, alertLevel, false);
+    }
+
+    @Override
+    public Page<AbnormalDeviceVO> getAbnormalDevicePage(long pageNum, long pageSize,
+                                                         String equipmentName, String equipmentCode,
+                                                         String equipmentTypeId, Integer alertLevel,
+                                                         Boolean dispatchableOnly) {
+        List<EquipmentBase> waterDevices = findWaterDevices();
+        if (CollUtil.isEmpty(waterDevices)) {
+            return emptyAbnormalPage(pageNum, pageSize, 0);
+        }
+
         LambdaQueryWrapper<EquipmentStatus> statusWrapper = new LambdaQueryWrapper<>();
         statusWrapper.and(w -> w.eq(EquipmentStatus::getAlarmStatus, 1)
                 .or().eq(EquipmentStatus::getCurrentStatus, 3));
 
         List<EquipmentStatus> allAbnormalStatus = equipmentStatusMapper.selectList(statusWrapper);
-        if (CollUtil.isEmpty(allAbnormalStatus)) {
-            Page<AbnormalDeviceVO> emptyPage = new Page<>(pageNum, pageSize, 0);
-            emptyPage.setRecords(new ArrayList<>());
-            return emptyPage;
-        }
-
-        List<String> equipmentIds = allAbnormalStatus.stream()
-                .map(EquipmentStatus::getEquipmentId).distinct().collect(Collectors.toList());
+        Map<String, EquipmentStatus> statusMap = allAbnormalStatus.stream()
+                .collect(Collectors.toMap(EquipmentStatus::getEquipmentId, s -> s, (a, b) -> a));
 
-        LambdaQueryWrapper<EquipmentBase> baseWrapper = new LambdaQueryWrapper<>();
-        baseWrapper.in(EquipmentBase::getEquipmentId, equipmentIds)
-                .like(StrUtil.isNotBlank(equipmentName), EquipmentBase::getEquipmentName, equipmentName)
-                .like(StrUtil.isNotBlank(equipmentCode), EquipmentBase::getEquipmentCode, equipmentCode)
-                .eq(StrUtil.isNotBlank(equipmentTypeId), EquipmentBase::getEquipmentTypeId, equipmentTypeId);
+        List<EquipmentBase> allDevices = waterDevices.stream()
+                .filter(device -> Optional.ofNullable(statusMap.get(device.getEquipmentId()))
+                        .map(status -> Objects.equals(status.getAlarmStatus(), 1)
+                                || Objects.equals(status.getCurrentStatus(), 3))
+                        .orElse(false))
+                .filter(device -> matchesLedgerFilters(device, equipmentName, equipmentCode, equipmentTypeId))
+                .collect(Collectors.toList());
 
-        List<EquipmentBase> allDevices = this.list(baseWrapper);
-        if (CollUtil.isEmpty(allDevices)) {
-            Page<AbnormalDeviceVO> emptyPage = new Page<>(pageNum, pageSize, 0);
-            emptyPage.setRecords(new ArrayList<>());
-            return emptyPage;
-        }
+        Map<String, AlarmData> alarmMap = getActiveAlarmMap(allDevices);
+        Map<String, WorkOrder> workOrderMap = getLatestWorkOrderMap(allDevices);
+        Map<String, EquipmentMaintain> maintainMap = getLatestMaintainMap(
+                allDevices.stream().map(EquipmentBase::getEquipmentId).collect(Collectors.toList())
+        );
 
-        int total = allDevices.size();
-        long offset = (pageNum - 1) * pageSize;
-        if (pageNum < 1 || pageSize < 1 || offset >= total) {
-            Page<AbnormalDeviceVO> emptyPage = new Page<>(pageNum, pageSize, total);
-            emptyPage.setRecords(new ArrayList<>());
-            return emptyPage;
+        List<EquipmentBase> filteredDevices = allDevices.stream()
+                .filter(device -> !Boolean.TRUE.equals(dispatchableOnly)
+                        || !workOrderMap.containsKey(device.getEquipmentId()))
+                .filter(device -> alertLevel == null
+                        || alertLevel.equals(resolveAlertLevel(device, statusMap.get(device.getEquipmentId()), alarmMap)))
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(filteredDevices)) {
+            return emptyAbnormalPage(pageNum, pageSize, filteredDevices.size());
         }
-        int fromIndex = (int) offset;
-        int toIndex = Math.min(fromIndex + (int) pageSize, total);
-        List<EquipmentBase> pageDevices = allDevices.subList(fromIndex, toIndex);
 
-        Map<String, EquipmentStatus> statusMap = allAbnormalStatus.stream()
-                .collect(Collectors.toMap(EquipmentStatus::getEquipmentId, s -> s, (a, b) -> a));
+        Page<EquipmentBase> basePage = paginate(filteredDevices, pageNum, pageSize);
+        List<EquipmentBase> pageDevices = basePage.getRecords();
         Map<String, EquipmentType> typeMap = getTypeMap(pageDevices);
-        Map<String, EquipmentMaintain> maintainMap = getLatestMaintainMap(
-                pageDevices.stream().map(EquipmentBase::getEquipmentId).collect(Collectors.toList())
-        );
 
         List<AbnormalDeviceVO> voList = new ArrayList<>();
         for (EquipmentBase base : pageDevices) {
             AbnormalDeviceVO vo = BeanUtil.copyProperties(base, AbnormalDeviceVO.class);
-            EquipmentStatus es = statusMap.get(base.getEquipmentId());
-            if (es != null) {
-                vo.setCurrentStatus(es.getCurrentStatus());
-                vo.setAlarmStatus(es.getAlarmStatus());
-                vo.setOnlineStatus(es.getOnlineStatus());
-                vo.setStatusUpdateTime(es.getStatusUpdateTime());
-            }
-            EquipmentType et = typeMap.get(base.getEquipmentTypeId());
-            if (et != null) {
-                vo.setEquipmentTypeName(et.getTypeName());
-            }
-            EquipmentMaintain maintain = maintainMap.get(base.getEquipmentId());
-            if (maintain != null) {
-                vo.setLatestMaintainId(maintain.getMaintainId());
-                vo.setLatestMaintainPerson(maintain.getMaintainPerson());
-            }
-            vo.setAlertLevel(calcAlertLevel(base.getEquipmentCode(), es));
-            fillGpsFields(vo, base.getLongitude(), base.getLatitude());
+            fillAbnormalDeviceVO(vo, base, statusMap.get(base.getEquipmentId()),
+                    typeMap.get(base.getEquipmentTypeId()), alarmMap.get(base.getEquipmentCode()),
+                    workOrderMap.get(base.getEquipmentId()), maintainMap.get(base.getEquipmentId()));
             voList.add(vo);
         }
 
-        Page<AbnormalDeviceVO> resultPage = new Page<>(pageNum, pageSize, total);
+        Page<AbnormalDeviceVO> resultPage = new Page<>(pageNum, pageSize, basePage.getTotal());
         resultPage.setRecords(voList);
         return resultPage;
     }
 
+    @Override
+    public List<AbnormalDeviceVO> getAbnormalDeviceExport(String equipmentName, String equipmentCode,
+                                                           String equipmentTypeId, Integer alertLevel) {
+        return getAbnormalDevicePage(1, Integer.MAX_VALUE, equipmentName, equipmentCode,
+                equipmentTypeId, alertLevel).getRecords();
+    }
+
     @Override
     public List<AbnormalDeviceVO> getAbnormalDeviceGisPoints(String equipmentTypeId) {
+        List<EquipmentBase> ownedDevices = findWaterDevices();
+        if (CollUtil.isEmpty(ownedDevices)) {
+            return new ArrayList<>();
+        }
+
         LambdaQueryWrapper<EquipmentStatus> statusWrapper = new LambdaQueryWrapper<>();
         statusWrapper.and(w -> w.eq(EquipmentStatus::getAlarmStatus, 1)
                 .or().eq(EquipmentStatus::getCurrentStatus, 3));
         List<EquipmentStatus> allAbnormalStatus = equipmentStatusMapper.selectList(statusWrapper);
-        if (CollUtil.isEmpty(allAbnormalStatus)) {
-            return new ArrayList<>();
-        }
-
-        List<String> equipmentIds = allAbnormalStatus.stream()
-                .map(EquipmentStatus::getEquipmentId).distinct().collect(Collectors.toList());
+        Map<String, EquipmentStatus> statusMap = allAbnormalStatus.stream()
+                .collect(Collectors.toMap(EquipmentStatus::getEquipmentId, s -> s, (a, b) -> a));
 
-        LambdaQueryWrapper<EquipmentBase> baseWrapper = new LambdaQueryWrapper<>();
-        baseWrapper.in(EquipmentBase::getEquipmentId, equipmentIds)
-                .eq(StrUtil.isNotBlank(equipmentTypeId), EquipmentBase::getEquipmentTypeId, equipmentTypeId)
-                .isNotNull(EquipmentBase::getLongitude)
-                .isNotNull(EquipmentBase::getLatitude);
-        List<EquipmentBase> devices = this.list(baseWrapper);
+        List<EquipmentBase> devices = ownedDevices.stream()
+                .filter(device -> StrUtil.isBlank(equipmentTypeId)
+                        || equipmentTypeId.equals(device.getEquipmentTypeId()))
+                .filter(device -> device.getLongitude() != null && device.getLatitude() != null)
+                .filter(device -> Optional.ofNullable(statusMap.get(device.getEquipmentId()))
+                        .map(status -> Objects.equals(status.getAlarmStatus(), 1)
+                                || Objects.equals(status.getCurrentStatus(), 3))
+                        .orElse(false))
+                .collect(Collectors.toList());
         if (CollUtil.isEmpty(devices)) {
             return new ArrayList<>();
         }
 
-        Map<String, EquipmentStatus> statusMap = allAbnormalStatus.stream()
-                .collect(Collectors.toMap(EquipmentStatus::getEquipmentId, s -> s, (a, b) -> a));
+        Map<String, AlarmData> alarmMap = getActiveAlarmMap(devices);
+        Map<String, WorkOrder> workOrderMap = getLatestWorkOrderMap(devices);
+        Map<String, EquipmentMaintain> maintainMap = getLatestMaintainMap(
+                devices.stream().map(EquipmentBase::getEquipmentId).collect(Collectors.toList())
+        );
         Map<String, EquipmentType> typeMap = getTypeMap(devices);
 
         List<AbnormalDeviceVO> result = new ArrayList<>();
         for (EquipmentBase base : devices) {
             AbnormalDeviceVO vo = BeanUtil.copyProperties(base, AbnormalDeviceVO.class);
-            EquipmentStatus es = statusMap.get(base.getEquipmentId());
-            if (es != null) {
-                vo.setCurrentStatus(es.getCurrentStatus());
-                vo.setAlarmStatus(es.getAlarmStatus());
-                vo.setOnlineStatus(es.getOnlineStatus());
-            }
-            EquipmentType et = typeMap.get(base.getEquipmentTypeId());
-            if (et != null) {
-                vo.setEquipmentTypeName(et.getTypeName());
-            }
-            vo.setAlertLevel(calcAlertLevel(base.getEquipmentCode(), es));
-            fillGpsFields(vo, base.getLongitude(), base.getLatitude());
+            fillAbnormalDeviceVO(vo, base, statusMap.get(base.getEquipmentId()),
+                    typeMap.get(base.getEquipmentTypeId()), alarmMap.get(base.getEquipmentCode()),
+                    workOrderMap.get(base.getEquipmentId()), maintainMap.get(base.getEquipmentId()));
             result.add(vo);
         }
         return result;
@@ -489,6 +572,9 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
 
     @Override
     public List<Map<String, Object>> getMaintainRecordList(String equipmentId) {
+        if (findWaterDeviceById(equipmentId) == null) {
+            return new ArrayList<>();
+        }
         List<EquipmentMaintain> list = equipmentMaintainMapper.selectList(
                 new LambdaQueryWrapper<EquipmentMaintain>()
                         .eq(EquipmentMaintain::getEquipmentId, equipmentId)
@@ -515,9 +601,9 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
         if (StrUtil.isBlank(vo.getEquipmentId())) {
             throw new RuntimeException("设备ID不能为空");
         }
-        EquipmentBase device = this.getById(vo.getEquipmentId());
+        EquipmentBase device = findWaterDeviceById(vo.getEquipmentId());
         if (device == null) {
-            throw new RuntimeException("设备不存在");
+            throw new RuntimeException("仅支持供水设备维护记录");
         }
         EquipmentMaintain maintain = BeanUtil.copyProperties(vo, EquipmentMaintain.class);
         return equipmentMaintainMapper.insert(maintain) > 0;
@@ -529,13 +615,30 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
         if (StrUtil.isBlank(vo.getMaintainId())) {
             throw new RuntimeException("维护记录ID不能为空");
         }
+        EquipmentMaintain existing = equipmentMaintainMapper.selectById(vo.getMaintainId());
+        if (existing == null) {
+            return false;
+        }
+        if (findWaterDeviceById(existing.getEquipmentId()) == null
+                || (StrUtil.isNotBlank(vo.getEquipmentId())
+                && !vo.getEquipmentId().equals(existing.getEquipmentId()))) {
+            throw new RuntimeException("仅支持供水设备维护记录");
+        }
         EquipmentMaintain maintain = BeanUtil.copyProperties(vo, EquipmentMaintain.class);
+        maintain.setEquipmentId(existing.getEquipmentId());
         return equipmentMaintainMapper.updateById(maintain) > 0;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean deleteMaintainRecord(String maintainId) {
+        EquipmentMaintain existing = equipmentMaintainMapper.selectById(maintainId);
+        if (existing == null) {
+            return false;
+        }
+        if (findWaterDeviceById(existing.getEquipmentId()) == null) {
+            throw new RuntimeException("仅支持供水设备维护记录");
+        }
         return equipmentMaintainMapper.deleteById(maintainId) > 0;
     }
 
@@ -544,85 +647,282 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
                                                                 String equipmentName, String maintainPerson,
                                                                 String maintainType, String status,
                                                                 String startTime, String endTime) {
+        LambdaQueryWrapper<EquipmentMaintain> wrapper = maintainRecordWrapper(maintainPerson, maintainType);
+        return buildMaintainRecordPage(equipmentMaintainMapper.selectList(wrapper),
+                pageNum, pageSize, equipmentName);
+    }
+
+    @Override
+    public Page<EquipmentMaintainDataVO> getMyMaintainRecordPage(long pageNum, long pageSize,
+                                                                   String maintainPerson,
+                                                                   String maintainType, String status,
+                                                                   String startTime, String endTime) {
+        LambdaQueryWrapper<EquipmentMaintain> wrapper = maintainRecordWrapper(maintainPerson, maintainType);
+        return buildMaintainRecordPage(equipmentMaintainMapper.selectList(wrapper),
+                pageNum, pageSize, null);
+    }
+
+    private LambdaQueryWrapper<EquipmentMaintain> maintainRecordWrapper(String maintainPerson,
+                                                                        String maintainType) {
         LambdaQueryWrapper<EquipmentMaintain> wrapper = new LambdaQueryWrapper<>();
         wrapper.like(StrUtil.isNotBlank(maintainPerson), EquipmentMaintain::getMaintainPerson, maintainPerson)
                 .eq(StrUtil.isNotBlank(maintainType), EquipmentMaintain::getMaintainType, maintainType)
                 .orderByDesc(EquipmentMaintain::getMaintainDate);
+        return wrapper;
+    }
 
-        Page<EquipmentMaintain> maintainPage = equipmentMaintainMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
+    private Page<EquipmentMaintainDataVO> buildMaintainRecordPage(List<EquipmentMaintain> records,
+                                                                  long pageNum, long pageSize,
+                                                                  String equipmentName) {
+        Map<String, EquipmentBase> deviceMap = findWaterDevices().stream()
+                .collect(Collectors.toMap(EquipmentBase::getEquipmentId, device -> device, (left, right) -> left));
+        List<EquipmentMaintain> ownedRecords = records == null ? Collections.emptyList() : records.stream()
+                .filter(record -> deviceMap.containsKey(record.getEquipmentId()))
+                .filter(record -> StrUtil.isBlank(equipmentName) || Optional.ofNullable(deviceMap.get(record.getEquipmentId()))
+                        .map(EquipmentBase::getEquipmentName)
+                        .map(name -> name.contains(equipmentName))
+                        .orElse(false))
+                .collect(Collectors.toList());
+
+        Page<EquipmentMaintain> maintainPage = paginate(ownedRecords, pageNum, pageSize);
         Page<EquipmentMaintainDataVO> resultPage = new Page<>(pageNum, pageSize, maintainPage.getTotal());
-
-        if (CollUtil.isEmpty(maintainPage.getRecords())) {
-            resultPage.setRecords(new ArrayList<>());
-            return resultPage;
-        }
-
-        List<String> equipmentIds = maintainPage.getRecords().stream()
-                .map(EquipmentMaintain::getEquipmentId).distinct().collect(Collectors.toList());
-        List<EquipmentBase> devices = this.listByIds(equipmentIds);
-        Map<String, EquipmentBase> deviceMap = devices.stream()
-                .collect(Collectors.toMap(EquipmentBase::getEquipmentId, d -> d, (a, b) -> a));
-
         List<EquipmentMaintainDataVO> voList = new ArrayList<>();
         for (EquipmentMaintain m : maintainPage.getRecords()) {
             EquipmentMaintainDataVO vo = BeanUtil.copyProperties(m, EquipmentMaintainDataVO.class);
             EquipmentBase device = deviceMap.get(m.getEquipmentId());
-            if (device != null) {
-                vo.setEquipmentCode(device.getEquipmentCode());
-                vo.setEquipmentName(device.getEquipmentName());
-                vo.setEquipmentLocation(device.getEquipmentLocation());
-                vo.setLongitude(device.getLongitude());
-                vo.setLatitude(device.getLatitude());
-            }
-            if (StrUtil.isNotBlank(equipmentName) && (vo.getEquipmentName() == null
-                    || !vo.getEquipmentName().contains(equipmentName))) {
-                continue;
-            }
+            vo.setEquipmentCode(device.getEquipmentCode());
+            vo.setEquipmentName(device.getEquipmentName());
+            vo.setEquipmentLocation(device.getEquipmentLocation());
+            vo.setLongitude(device.getLongitude());
+            vo.setLatitude(device.getLatitude());
             voList.add(vo);
         }
         resultPage.setRecords(voList);
         return resultPage;
     }
 
-    @Override
-    public Page<EquipmentMaintainDataVO> getMyMaintainRecordPage(long pageNum, long pageSize,
-                                                                   String maintainPerson,
-                                                                   String maintainType, String status,
-                                                                   String startTime, String endTime) {
-        LambdaQueryWrapper<EquipmentMaintain> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StrUtil.isNotBlank(maintainPerson), EquipmentMaintain::getMaintainPerson, maintainPerson)
-                .eq(StrUtil.isNotBlank(maintainType), EquipmentMaintain::getMaintainType, maintainType)
-                .orderByDesc(EquipmentMaintain::getMaintainDate);
+    private List<EquipmentBase> findWaterDevices() {
+        return equipmentBaseService.findByTopLevelType(waterTopLevelTypeName);
+    }
 
-        Page<EquipmentMaintain> maintainPage = equipmentMaintainMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
-        Page<EquipmentMaintainDataVO> resultPage = new Page<>(pageNum, pageSize, maintainPage.getTotal());
+    private EquipmentBase findWaterDeviceById(String equipmentId) {
+        if (StrUtil.isBlank(equipmentId)) {
+            return null;
+        }
+        return findWaterDevices().stream()
+                .filter(device -> equipmentId.equals(device.getEquipmentId()))
+                .findFirst()
+                .orElse(null);
+    }
 
-        if (CollUtil.isEmpty(maintainPage.getRecords())) {
-            resultPage.setRecords(new ArrayList<>());
-            return resultPage;
+    private Set<String> resolveOwnedTypeRecordIds(List<EquipmentType> allTypes, Set<String> usedTypeIds) {
+        Map<String, List<EquipmentType>> childrenByParent = allTypes.stream()
+                .filter(type -> StrUtil.isNotBlank(type.getParentTypeId()))
+                .collect(Collectors.groupingBy(EquipmentType::getParentTypeId));
+        Map<String, EquipmentType> typesById = allTypes.stream()
+                .collect(Collectors.toMap(EquipmentType::getId, type -> type, (left, right) -> left));
+
+        Set<String> ownedIds = new HashSet<>();
+        for (String usedTypeId : usedTypeIds) {
+            EquipmentType type = typesById.get(usedTypeId);
+            while (type != null && type.getParentTypeId() != null
+                    && !"0".equals(type.getParentTypeId())
+                    && ownedIds.add(type.getId())) {
+                type = typesById.get(type.getParentTypeId());
+            }
+            if (type != null) {
+                ownedIds.add(type.getId());
+            }
         }
 
-        List<String> equipmentIds = maintainPage.getRecords().stream()
-                .map(EquipmentMaintain::getEquipmentId).distinct().collect(Collectors.toList());
-        List<EquipmentBase> devices = this.listByIds(equipmentIds);
-        Map<String, EquipmentBase> deviceMap = devices.stream()
-                .collect(Collectors.toMap(EquipmentBase::getEquipmentId, d -> d, (a, b) -> a));
+        Set<String> expandedIds = new HashSet<>(ownedIds);
+        for (String ownedId : ownedIds) {
+            expandChildTypeIds(ownedId, childrenByParent, expandedIds);
+        }
+        return expandedIds;
+    }
 
-        List<EquipmentMaintainDataVO> voList = new ArrayList<>();
-        for (EquipmentMaintain m : maintainPage.getRecords()) {
-            EquipmentMaintainDataVO vo = BeanUtil.copyProperties(m, EquipmentMaintainDataVO.class);
-            EquipmentBase device = deviceMap.get(m.getEquipmentId());
-            if (device != null) {
-                vo.setEquipmentCode(device.getEquipmentCode());
-                vo.setEquipmentName(device.getEquipmentName());
-                vo.setEquipmentLocation(device.getEquipmentLocation());
-                vo.setLongitude(device.getLongitude());
-                vo.setLatitude(device.getLatitude());
+    private void expandChildTypeIds(String parentId,
+                                    Map<String, List<EquipmentType>> childrenByParent,
+                                    Set<String> result) {
+        for (EquipmentType child : childrenByParent.getOrDefault(parentId, Collections.emptyList())) {
+            if (result.add(child.getId())) {
+                expandChildTypeIds(child.getId(), childrenByParent, result);
             }
-            voList.add(vo);
         }
-        resultPage.setRecords(voList);
-        return resultPage;
+    }
+
+    private boolean matchesLedgerFilters(EquipmentBase device, String equipmentName,
+                                         String equipmentCode, String equipmentTypeId) {
+        return (StrUtil.isBlank(equipmentName)
+                || (device.getEquipmentName() != null && device.getEquipmentName().contains(equipmentName)))
+                && (StrUtil.isBlank(equipmentCode)
+                || (device.getEquipmentCode() != null && device.getEquipmentCode().contains(equipmentCode)))
+                && (StrUtil.isBlank(equipmentTypeId) || equipmentTypeId.equals(device.getEquipmentTypeId()));
+    }
+
+    private <T> Page<T> paginate(List<T> records, long pageNum, long pageSize) {
+        Page<T> page = new Page<>(pageNum, pageSize, records.size());
+        if (pageNum < 1 || pageSize < 1) {
+            page.setRecords(new ArrayList<>());
+            return page;
+        }
+        int fromIndex = (int) Math.min((pageNum - 1) * pageSize, records.size());
+        int toIndex = (int) Math.min(fromIndex + pageSize, records.size());
+        page.setRecords(new ArrayList<>(records.subList(fromIndex, toIndex)));
+        return page;
+    }
+
+    private Page<DeviceGisVO> emptyPage(long pageNum, long pageSize) {
+        Page<DeviceGisVO> page = new Page<>(pageNum, pageSize, 0);
+        page.setRecords(new ArrayList<>());
+        return page;
+    }
+
+    private Page<AbnormalDeviceVO> emptyAbnormalPage(long pageNum, long pageSize, long total) {
+        Page<AbnormalDeviceVO> page = new Page<>(pageNum, pageSize, total);
+        page.setRecords(new ArrayList<>());
+        return page;
+    }
+
+    private Map<String, AlarmData> getActiveAlarmMap(List<EquipmentBase> devices) {
+        if (CollUtil.isEmpty(devices)) {
+            return new HashMap<>();
+        }
+        List<String> deviceCodes = devices.stream()
+                .map(EquipmentBase::getEquipmentCode)
+                .filter(StrUtil::isNotBlank)
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(deviceCodes)) {
+            return new HashMap<>();
+        }
+        List<AlarmData> alarms = alarmDataMapper.selectList(
+                new LambdaQueryWrapper<AlarmData>()
+                        .in(AlarmData::getDeviceCode, deviceCodes)
+                        .eq(AlarmData::getAlarmStatus, 0)
+                        .orderByAsc(AlarmData::getAlarmLevel)
+                        .orderByDesc(AlarmData::getAlarmTime)
+        );
+        Map<String, AlarmData> result = new HashMap<>();
+        for (AlarmData alarm : alarms) {
+            AlarmData existing = result.get(alarm.getDeviceCode());
+            if (existing == null || isMoreSevereAlarm(alarm, existing)) {
+                result.put(alarm.getDeviceCode(), alarm);
+            }
+        }
+        return result;
+    }
+
+    private boolean isMoreSevereAlarm(AlarmData candidate, AlarmData existing) {
+        if (candidate.getAlarmLevel() == null) {
+            return false;
+        }
+        if (existing.getAlarmLevel() == null || candidate.getAlarmLevel() < existing.getAlarmLevel()) {
+            return true;
+        }
+        return Objects.equals(candidate.getAlarmLevel(), existing.getAlarmLevel())
+                && candidate.getAlarmTime() != null
+                && (existing.getAlarmTime() == null || candidate.getAlarmTime().isAfter(existing.getAlarmTime()));
+    }
+
+    private Map<String, WorkOrder> getLatestWorkOrderMap(List<EquipmentBase> devices) {
+        if (CollUtil.isEmpty(devices)) {
+            return new HashMap<>();
+        }
+        List<String> equipmentIds = devices.stream()
+                .map(EquipmentBase::getEquipmentId)
+                .filter(StrUtil::isNotBlank)
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(equipmentIds)) {
+            return new HashMap<>();
+        }
+        List<WorkOrder> orders = workOrderMapper.selectList(
+                new LambdaQueryWrapper<WorkOrder>()
+                        .in(WorkOrder::getDeviceId, equipmentIds)
+                        .eq(WorkOrder::getOrderType, 1)
+                        .notIn(WorkOrder::getOrderStatus, 6, 7)
+                        .orderByDesc(WorkOrder::getCreateTime)
+        );
+        Map<String, WorkOrder> result = new HashMap<>();
+        for (WorkOrder order : orders) {
+            if (!Objects.equals(order.getOrderType(), 1)
+                    || Objects.equals(order.getOrderStatus(), 6)
+                    || Objects.equals(order.getOrderStatus(), 7)) {
+                continue;
+            }
+            result.putIfAbsent(order.getDeviceId(), order);
+        }
+        return result;
+    }
+
+    private Integer resolveAlertLevel(EquipmentBase device, EquipmentStatus status, Map<String, AlarmData> alarmMap) {
+        if (status == null || status.getAlarmStatus() == null || status.getAlarmStatus() == 0) {
+            return null;
+        }
+        AlarmData alarm = alarmMap.get(device.getEquipmentCode());
+        return alarm != null && alarm.getAlarmLevel() != null ? alarm.getAlarmLevel() : 4;
+    }
+
+    private void fillAbnormalDeviceVO(AbnormalDeviceVO vo, EquipmentBase base, EquipmentStatus status,
+                                      EquipmentType type, AlarmData alarm, WorkOrder workOrder,
+                                      EquipmentMaintain maintain) {
+        if (status != null) {
+            vo.setCurrentStatus(status.getCurrentStatus());
+            vo.setAlarmStatus(status.getAlarmStatus());
+            vo.setOnlineStatus(status.getOnlineStatus());
+            vo.setStatusUpdateTime(status.getStatusUpdateTime());
+        }
+        if (type != null) {
+            vo.setEquipmentTypeName(type.getTypeName());
+        }
+        if (alarm != null) {
+            vo.setAbnormalReason(StrUtil.isNotBlank(alarm.getWarningType())
+                    ? alarm.getWarningType() : "设备报警");
+            vo.setAlarmTime(alarm.getAlarmTime());
+        } else if (Objects.equals(vo.getCurrentStatus(), 3)) {
+            vo.setAbnormalReason("设备维修中");
+        } else {
+            vo.setAbnormalReason("设备异常");
+        }
+        if (workOrder != null) {
+            vo.setRelatedWorkOrderNo(workOrder.getOrderNo());
+            vo.setRelatedWorkOrderStatus(workOrder.getOrderStatus());
+            vo.setRelatedWorkOrderPerson(resolveWorkOrderPerson(workOrder));
+        }
+        if (maintain != null) {
+            vo.setLatestMaintainId(maintain.getMaintainId());
+            vo.setLatestMaintainPerson(maintain.getMaintainPerson());
+        }
+        vo.setAlertLevel(resolveAlertLevel(base, status,
+                alarm == null ? new HashMap<>() : Collections.singletonMap(alarm.getDeviceCode(), alarm)));
+        fillGpsFields(vo, base.getLongitude(), base.getLatitude());
+    }
+
+    private String resolveWorkOrderPerson(WorkOrder workOrder) {
+        if (workOrder == null || StrUtil.isBlank(workOrder.getReceiveUser())) {
+            return null;
+        }
+        return Arrays.stream(workOrder.getReceiveUser().split(","))
+                .map(String::trim)
+                .filter(StrUtil::isNotBlank)
+                .map(this::resolveUserName)
+                .filter(StrUtil::isNotBlank)
+                .collect(Collectors.joining("、"));
+    }
+
+    private String resolveUserName(String userId) {
+        try {
+            if (sysUserService == null) {
+                return userId;
+            }
+            SysUser user = sysUserService.selectUserById(Long.valueOf(userId));
+            if (user == null) {
+                return null;
+            }
+            return StrUtil.isNotBlank(user.getNickName()) ? user.getNickName() : user.getUserName();
+        } catch (NumberFormatException e) {
+            return userId;
+        }
     }
 
     private Map<String, EquipmentStatus> getStatusMap(List<String> equipmentIds) {
@@ -648,7 +948,7 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
         }
         List<EquipmentType> types = equipmentTypeMapper.selectBatchIds(typeIds);
         return types.stream()
-                .collect(Collectors.toMap(EquipmentType::getTypeId, t -> t, (a, b) -> a));
+                .collect(Collectors.toMap(EquipmentType::getId, t -> t, (a, b) -> a));
     }
 
     private Map<String, EquipmentMaintain> getLatestMaintainMap(List<String> equipmentIds) {
@@ -799,12 +1099,9 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
         return result;
     }
 
-    private int calcAlertLevel(String deviceCode, EquipmentStatus status) {
+    private Integer calcAlertLevel(String deviceCode, EquipmentStatus status) {
         if (status == null || status.getAlarmStatus() == null || status.getAlarmStatus() == 0) {
-            if (status != null && status.getCurrentStatus() != null && status.getCurrentStatus() == 3) {
-                return 3;
-            }
-            return 4;
+            return null;
         }
         int maxLevel = 4;
         try {
@@ -812,7 +1109,7 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
                     new LambdaQueryWrapper<com.zksy.base.alarm.domain.AlarmData>()
                             .eq(com.zksy.base.alarm.domain.AlarmData::getDeviceCode, deviceCode)
                             .eq(com.zksy.base.alarm.domain.AlarmData::getAlarmStatus, 0)
-                            .orderByDesc(com.zksy.base.alarm.domain.AlarmData::getAlarmLevel)
+                            .orderByAsc(com.zksy.base.alarm.domain.AlarmData::getAlarmLevel)
                             .last("LIMIT 1")
             );
             if (CollUtil.isNotEmpty(alarms) && alarms.get(0).getAlarmLevel() != null) {

+ 4 - 1
pipe-network-service/zksy-system/src/main/resources/mapper/system/SysUserMapper.xml

@@ -67,6 +67,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<if test="userName != null and userName != ''">
 			AND u.user_name like concat('%', #{userName}, '%')
 		</if>
+		<if test="nickName != null and nickName != ''">
+			AND u.nick_name like concat('%', #{nickName}, '%')
+		</if>
 		<if test="status != null and status != ''">
 			AND u.status = #{status}
 		</if>
@@ -227,4 +230,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach> 
  	</delete>
 	
-</mapper> 
+</mapper>

+ 62 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/base/service/impl/BusinessWorkOrderServiceImplTest.java

@@ -0,0 +1,62 @@
+package com.zksy.base.service.impl;
+
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.domain.WorkOrderLog;
+import com.zksy.base.mapper.WorkOrderLogMapper;
+import com.zksy.base.mapper.WorkOrderMapper;
+import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.system.service.ISysUserService;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+class BusinessWorkOrderServiceImplTest {
+
+    @Test
+    void dispatchRejectsMissingOrInactiveReceiverBeforeUpdatingOrder() {
+        BusinessWorkOrderServiceImpl service = new BusinessWorkOrderServiceImpl();
+        WorkOrderMapper orderMapper = mock(WorkOrderMapper.class);
+        ISysUserService users = mock(ISysUserService.class);
+        ReflectionTestUtils.setField(service, "baseMapper", orderMapper);
+        ReflectionTestUtils.setField(service, "sysUserService", users);
+        when(users.selectUserById(88L)).thenReturn(null);
+
+        assertThrows(ServiceException.class, () -> service.dispatchOrder(1L, 88L, null, 7L, "operator"));
+        verify(orderMapper, never()).selectById(any());
+        verify(orderMapper, never()).updateById(any(WorkOrder.class));
+    }
+
+    @Test
+    void dispatchAcceptsActiveNonDeletedReceiverAndMovesOrderToStatusTwo() {
+        BusinessWorkOrderServiceImpl service = new BusinessWorkOrderServiceImpl();
+        WorkOrderMapper orderMapper = mock(WorkOrderMapper.class);
+        WorkOrderLogMapper logs = mock(WorkOrderLogMapper.class);
+        ISysUserService users = mock(ISysUserService.class);
+        ReflectionTestUtils.setField(service, "baseMapper", orderMapper);
+        ReflectionTestUtils.setField(service, "workOrderLogMapper", logs);
+        ReflectionTestUtils.setField(service, "sysUserService", users);
+
+        SysUser user = new SysUser();
+        user.setUserId(88L);
+        user.setStatus("0");
+        user.setDelFlag("0");
+        when(users.selectUserById(88L)).thenReturn(user);
+        WorkOrder pending = new WorkOrder();
+        pending.setOrderId(1L);
+        pending.setOrderStatus(1);
+        when(orderMapper.selectById(1L)).thenReturn(pending);
+
+        assertTrue(service.dispatchOrder(1L, 88L, "处理", 7L, "operator"));
+        ArgumentCaptor<WorkOrder> update = ArgumentCaptor.forClass(WorkOrder.class);
+        verify(orderMapper).updateById(update.capture());
+        org.junit.jupiter.api.Assertions.assertEquals(2, update.getValue().getOrderStatus());
+        org.junit.jupiter.api.Assertions.assertEquals("88", update.getValue().getReceiveUser());
+        verify(logs).insert(any(WorkOrderLog.class));
+    }
+}

+ 690 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/base/service/impl/WaterSupplyDeviceServiceImplTest.java

@@ -0,0 +1,690 @@
+package com.zksy.base.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.alarm.mapper.AlarmDataMapper;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentMaintain;
+import com.zksy.base.domain.EquipmentStatus;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.domain.vo.AbnormalDeviceVO;
+import com.zksy.base.domain.vo.DeviceGisVO;
+import com.zksy.base.domain.vo.DeviceTypeStatisticsVO;
+import com.zksy.base.domain.vo.MaintainOrderVO;
+import com.zksy.base.mapper.EquipmentMaintainMapper;
+import com.zksy.base.mapper.EquipmentStatusMapper;
+import com.zksy.base.mapper.EquipmentTypeMapper;
+import com.zksy.base.mapper.WorkOrderMapper;
+import com.zksy.base.service.EquipmentBaseService;
+import com.zksy.common.core.domain.entity.SysUser;
+import com.zksy.system.service.ISysUserService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class WaterSupplyDeviceServiceImplTest {
+
+    @BeforeAll
+    static void initializeMybatisPlusLambdaCache() {
+        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, EquipmentBase.class);
+        TableInfoHelper.initTableInfo(assistant, EquipmentStatus.class);
+        TableInfoHelper.initTableInfo(assistant, AlarmData.class);
+        TableInfoHelper.initTableInfo(assistant, WorkOrder.class);
+    }
+
+    private WaterSupplyDeviceServiceImpl service;
+    private EquipmentBaseService equipmentBaseService;
+    private EquipmentTypeMapper equipmentTypeMapper;
+    private EquipmentStatusMapper equipmentStatusMapper;
+    private EquipmentMaintainMapper equipmentMaintainMapper;
+    private AlarmDataMapper alarmDataMapper;
+    private WorkOrderMapper workOrderMapper;
+    private com.zksy.base.service.BusinessWorkOrderService businessWorkOrderService;
+
+    private EquipmentBase pressureDevice;
+    private EquipmentBase repairDevice;
+    private EquipmentBase gasDevice;
+
+    @BeforeEach
+    void setUp() {
+        service = Mockito.spy(new WaterSupplyDeviceServiceImpl());
+        equipmentBaseService = mock(EquipmentBaseService.class);
+        equipmentTypeMapper = mock(EquipmentTypeMapper.class);
+        equipmentStatusMapper = mock(EquipmentStatusMapper.class);
+        equipmentMaintainMapper = mock(EquipmentMaintainMapper.class);
+        alarmDataMapper = mock(AlarmDataMapper.class);
+        workOrderMapper = mock(WorkOrderMapper.class);
+        businessWorkOrderService = mock(com.zksy.base.service.BusinessWorkOrderService.class);
+
+        ReflectionTestUtils.setField(service, "equipmentBaseService", equipmentBaseService);
+        ReflectionTestUtils.setField(service, "equipmentTypeMapper", equipmentTypeMapper);
+        ReflectionTestUtils.setField(service, "equipmentStatusMapper", equipmentStatusMapper);
+        ReflectionTestUtils.setField(service, "equipmentMaintainMapper", equipmentMaintainMapper);
+        ReflectionTestUtils.setField(service, "alarmDataMapper", alarmDataMapper);
+        ReflectionTestUtils.setField(service, "workOrderMapper", workOrderMapper);
+        ReflectionTestUtils.setField(service, "businessWorkOrderService", businessWorkOrderService);
+
+        pressureDevice = device("water-pressure-1", "WS-PRESSURE-001", "供水压力监测点", "water-pressure");
+        repairDevice = device("water-flow-1", "WS-FLOW-001", "供水流量监测点", "water-flow");
+        gasDevice = device("gas-1", "GAS-001", "燃气监测点", "gas-root");
+
+        when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Arrays.asList(
+                pressureDevice, repairDevice));
+        when(equipmentTypeMapper.selectList(any())).thenReturn(Arrays.asList(
+                type("water-root", "供水", "0"),
+                type("water-pressure", "供水压力计", "water-root"),
+                type("water-flow", "供水流量计", "water-root"),
+                type("gas-root", "燃气设备", "0")));
+        when(equipmentTypeMapper.selectBatchIds(any())).thenReturn(Arrays.asList(
+                type("water-pressure", "供水压力计", "water-root"),
+                type("water-flow", "供水流量计", "water-root"),
+                type("gas-root", "燃气设备", "0")));
+    }
+
+    @Test
+    void typeStatisticsCountOnlyTheWaterTypeSubtree() {
+        EquipmentBase secondPressure = device("water-pressure-2", "WS-PRESSURE-002",
+                "第二供水压力监测点", "water-pressure");
+        when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Arrays.asList(
+                pressureDevice, secondPressure, repairDevice));
+        List<DeviceTypeStatisticsVO> roots = service.getDeviceTypeStatistics();
+
+        assertEquals(1, roots.size());
+        assertEquals("water-root", roots.get(0).getTypeId());
+        assertEquals(3, roots.get(0).getDeviceCount());
+        List<String> childTypeIds = roots.get(0).getChildren().stream()
+                .map(DeviceTypeStatisticsVO::getTypeId)
+                .sorted()
+                .collect(Collectors.toList());
+        assertEquals(Arrays.asList("water-flow", "water-pressure"), childTypeIds);
+    }
+
+    @Test
+    void ledgerResolvesTypeNameByEquipmentTypePrimaryKey() {
+        pressureDevice.setEquipmentTypeId("db-pressure-id");
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
+                status(pressureDevice.getEquipmentId(), 1, 0, 1)));
+        EquipmentType resolvedType = type("db-pressure-id", "business-pressure", "water-root");
+        resolvedType.setTypeId("business-pressure-code");
+        when(equipmentTypeMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(resolvedType));
+
+        Page<DeviceGisVO> result = service.getDeviceLedgerPage(
+                1, 10, null, null, null, null, null, null);
+
+        assertEquals("business-pressure", result.getRecords().get(0).getEquipmentTypeName());
+    }
+
+    @Test
+    void ledgerPageAppliesWaterOwnershipAndCurrentStatusFilter() {
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Arrays.asList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1),
+                status(repairDevice.getEquipmentId(), 3, 0, 0),
+                status(gasDevice.getEquipmentId(), 1, 1, 1)));
+
+        Page<DeviceGisVO> result = service.getDeviceLedgerPage(
+                1, 10, null, null, null, 1, null, null);
+
+        assertEquals(1, result.getTotal());
+        assertEquals(Collections.singletonList("water-pressure-1"), result.getRecords().stream()
+                .map(DeviceGisVO::getEquipmentId)
+                .collect(Collectors.toList()));
+        assertEquals(1, result.getRecords().get(0).getCurrentStatus());
+    }
+
+    @Test
+    void ledgerExportReturnsAllMatchingWaterDevicesWithoutPagination() {
+        EquipmentBase secondPressure = device("water-pressure-2", "WS-PRESSURE-002",
+                "第二供水压力监测点", "water-pressure");
+        when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Arrays.asList(
+                pressureDevice, secondPressure, repairDevice));
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Arrays.asList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1),
+                status(secondPressure.getEquipmentId(), 1, 0, 1),
+                status(repairDevice.getEquipmentId(), 3, 0, 0),
+                status(gasDevice.getEquipmentId(), 1, 1, 1)));
+
+        List<DeviceGisVO> result = service.getDeviceLedgerExport(
+                null, null, null, 1, null, null);
+
+        assertEquals(Arrays.asList("water-pressure-1", "water-pressure-2"), result.stream()
+                .map(DeviceGisVO::getEquipmentId)
+                .collect(Collectors.toList()));
+    }
+
+    @Test
+    void ledgerPageAppliesAlarmStatusAndAlertLevelFilters() {
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Arrays.asList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1),
+                status(repairDevice.getEquipmentId(), 1, 0, 1),
+                status(gasDevice.getEquipmentId(), 1, 1, 1)));
+        when(alarmDataMapper.selectList(any())).thenReturn(Arrays.asList(
+                alarm(pressureDevice.getEquipmentCode(), "供水压力超限", 1),
+                alarm(gasDevice.getEquipmentCode(), "燃气浓度超限", 2)));
+
+        Page<DeviceGisVO> result = service.getDeviceLedgerPage(
+                1, 10, null, null, null, null, 1, 1);
+
+        assertEquals(1, result.getTotal());
+        assertEquals(Collections.singletonList("water-pressure-1"), result.getRecords().stream()
+                .map(DeviceGisVO::getEquipmentId)
+                .collect(Collectors.toList()));
+        assertEquals(1, result.getRecords().get(0).getAlarmStatus());
+        assertEquals(1, result.getRecords().get(0).getAlertLevel());
+    }
+
+    @Test
+    void abnormalPageUsesAlarmReasonAlertLevelAndUnifiedWorkOrder() {
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Arrays.asList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1),
+                status(repairDevice.getEquipmentId(), 3, 0, 0),
+                status(gasDevice.getEquipmentId(), 1, 1, 1)));
+        when(alarmDataMapper.selectList(any())).thenReturn(Arrays.asList(
+                alarm(pressureDevice.getEquipmentCode(), "供水压力超限", 1),
+                alarm(gasDevice.getEquipmentCode(), "燃气浓度超限", 2)));
+        when(workOrderMapper.selectList(any())).thenReturn(Arrays.asList(
+                workOrder(pressureDevice.getEquipmentId(), "WO-WS-001", 4, "张三"),
+                workOrder(repairDevice.getEquipmentId(), "WO-WS-002", 4, "李四"),
+                workOrder(gasDevice.getEquipmentId(), "WO-GAS-001", 2)));
+
+        Page<AbnormalDeviceVO> result = service.getAbnormalDevicePage(
+                1, 10, null, null, null, null);
+
+        assertEquals(2, result.getTotal());
+        assertEquals(Arrays.asList("water-pressure-1", "water-flow-1"), result.getRecords().stream()
+                .map(AbnormalDeviceVO::getEquipmentId)
+                .collect(Collectors.toList()));
+
+        AbnormalDeviceVO alarmRow = result.getRecords().get(0);
+        assertEquals(1, alarmRow.getAlarmStatus());
+        assertEquals(1, alarmRow.getCurrentStatus());
+        assertEquals(1, alarmRow.getOnlineStatus());
+        assertEquals(1, alarmRow.getAlertLevel());
+        assertEquals("供水压力超限", ReflectionTestUtils.getField(alarmRow, "abnormalReason"));
+        assertEquals("WO-WS-001", ReflectionTestUtils.getField(alarmRow, "relatedWorkOrderNo"));
+        assertEquals(4, ReflectionTestUtils.getField(alarmRow, "relatedWorkOrderStatus"));
+        assertEquals("张三", ReflectionTestUtils.getField(alarmRow, "relatedWorkOrderPerson"));
+
+        AbnormalDeviceVO repairRow = result.getRecords().get(1);
+        assertEquals(0, repairRow.getAlarmStatus());
+        assertEquals(3, repairRow.getCurrentStatus());
+        assertEquals(0, repairRow.getOnlineStatus());
+        assertNull(repairRow.getAlertLevel());
+        assertEquals("设备维修中", ReflectionTestUtils.getField(repairRow, "abnormalReason"));
+        assertEquals("WO-WS-002", ReflectionTestUtils.getField(repairRow, "relatedWorkOrderNo"));
+        assertEquals(4, ReflectionTestUtils.getField(repairRow, "relatedWorkOrderStatus"));
+        assertEquals("李四", ReflectionTestUtils.getField(repairRow, "relatedWorkOrderPerson"));
+
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<Wrapper<WorkOrder>> orderWrapperCaptor =
+                ArgumentCaptor.forClass(Wrapper.class);
+        verify(workOrderMapper).selectList(orderWrapperCaptor.capture());
+        String orderWrapperSql = orderWrapperCaptor.getValue().getSqlSegment();
+        assertTrue(orderWrapperSql.contains("order_type"), "异常设备关联工单必须限定故障维修: " + orderWrapperSql);
+        assertTrue(orderWrapperSql.contains("order_status"), "异常设备关联工单必须排除已结案工单: " + orderWrapperSql);
+        AbstractWrapper<?, ?, ?> orderWrapper = (AbstractWrapper<?, ?, ?>) orderWrapperCaptor.getValue();
+        assertTrue(orderWrapper.getParamNameValuePairs().containsValue(1), "关联工单类型必须为 1-故障维修");
+        assertTrue(orderWrapper.getParamNameValuePairs().containsValue(6), "关联工单必须排除 6-已结案");
+        assertTrue(orderWrapper.getParamNameValuePairs().containsValue(7), "关联工单必须排除 7-已驳回");
+    }
+
+    @Test
+    void abnormalPageMapsMultipleWorkOrderReceiversToDisplayNames() {
+        ISysUserService sysUserService = mock(ISysUserService.class);
+        ReflectionTestUtils.setField(service, "sysUserService", sysUserService);
+        when(sysUserService.selectUserById(1L)).thenReturn(sysUser(1L, "zhangsan", "张三丰"));
+        when(sysUserService.selectUserById(2L)).thenReturn(sysUser(2L, "lisi", null));
+
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1)));
+        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(
+                alarm(pressureDevice.getEquipmentCode(), "供水压力超限", 1)));
+        when(workOrderMapper.selectList(any())).thenReturn(Collections.singletonList(
+                workOrder(pressureDevice.getEquipmentId(), "WO-WS-003", 4, "1, 2")));
+
+        Page<AbnormalDeviceVO> result = service.getAbnormalDevicePage(
+                1, 10, null, null, null, null);
+
+        assertEquals(1, result.getTotal());
+        assertEquals("张三丰、lisi", ReflectionTestUtils.getField(
+                result.getRecords().get(0), "relatedWorkOrderPerson"));
+    }
+
+    @Test
+    void abnormalExportReturnsAllWaterAbnormalDevicesWithoutPagination() {
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Arrays.asList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1),
+                status(repairDevice.getEquipmentId(), 3, 0, 0),
+                status(gasDevice.getEquipmentId(), 1, 1, 1)));
+        when(alarmDataMapper.selectList(any())).thenReturn(Arrays.asList(
+                alarm(pressureDevice.getEquipmentCode(), "供水压力超限", 1),
+                alarm(gasDevice.getEquipmentCode(), "燃气浓度超限", 2)));
+        when(workOrderMapper.selectList(any())).thenReturn(Arrays.asList(
+                workOrder(pressureDevice.getEquipmentId(), "WO-WS-001", 4),
+                workOrder(repairDevice.getEquipmentId(), "WO-WS-002", 6),
+                workOrder(gasDevice.getEquipmentId(), "WO-GAS-001", 2)));
+
+        List<AbnormalDeviceVO> result = service.getAbnormalDeviceExport(
+                null, null, null, null);
+
+        assertEquals(Arrays.asList("water-pressure-1", "water-flow-1"), result.stream()
+                .map(AbnormalDeviceVO::getEquipmentId)
+                .collect(Collectors.toList()));
+        assertEquals("供水压力超限", result.get(0).getAbnormalReason());
+        assertEquals("设备维修中", result.get(1).getAbnormalReason());
+        assertNull(result.get(1).getAlertLevel());
+    }
+
+    @Test
+    void abnormalAlertLevelFilterUsesActiveAlarmLevel() {
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1)));
+        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(
+                alarm(pressureDevice.getEquipmentCode(), "供水压力超限", 1)));
+
+        Page<AbnormalDeviceVO> result = service.getAbnormalDevicePage(
+                1, 10, null, null, null, 2);
+
+        assertEquals(0, result.getTotal());
+        assertTrue(result.getRecords().isEmpty());
+    }
+
+    @Test
+    void multipleActiveAlarmsChooseMostSevereLevelOne() {
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1)));
+        when(alarmDataMapper.selectList(any())).thenReturn(Arrays.asList(
+                alarm(pressureDevice.getEquipmentCode(), "一般告警", 3),
+                alarm(pressureDevice.getEquipmentCode(), "严重告警", 1),
+                alarm(pressureDevice.getEquipmentCode(), "较重告警", 2)));
+
+        Page<AbnormalDeviceVO> result = service.getAbnormalDevicePage(1, 10, null, null, null, null);
+
+        assertEquals(1, result.getRecords().get(0).getAlertLevel());
+        assertEquals("严重告警", result.getRecords().get(0).getAbnormalReason());
+    }
+
+    @Test
+    void abnormalPageExcludesClosedAndRejectedWorkOrders() {
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1)));
+        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(
+                alarm(pressureDevice.getEquipmentCode(), "供水压力超限", 1)));
+        when(workOrderMapper.selectList(any())).thenReturn(Arrays.asList(
+                workOrder(pressureDevice.getEquipmentId(), "WO-REJECTED", 7),
+                workOrder(pressureDevice.getEquipmentId(), "WO-CLOSED", 6)));
+
+        Page<AbnormalDeviceVO> result = service.getAbnormalDevicePage(1, 10, null, null, null, null);
+
+        assertNull(result.getRecords().get(0).getRelatedWorkOrderNo());
+        ArgumentCaptor<Wrapper<WorkOrder>> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
+        verify(workOrderMapper).selectList(wrapperCaptor.capture());
+        AbstractWrapper<?, ?, ?> wrapper = (AbstractWrapper<?, ?, ?>) wrapperCaptor.getValue();
+        assertTrue(wrapper.getSqlSegment().contains("order_status"));
+    }
+
+    @Test
+    void dispatchableOnlyExcludesDevicesWithActiveFaultWorkOrders() {
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Arrays.asList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1),
+                status(repairDevice.getEquipmentId(), 3, 0, 1)));
+        when(alarmDataMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(workOrderMapper.selectList(any())).thenReturn(Arrays.asList(
+                workOrder(pressureDevice.getEquipmentId(), "WO-ACTIVE", 2),
+                workOrder(repairDevice.getEquipmentId(), "WO-CLOSED", 6)));
+
+        Page<AbnormalDeviceVO> result = service.getAbnormalDevicePage(
+                1, 10, null, null, null, null, true);
+
+        assertEquals(Collections.singletonList(repairDevice.getEquipmentId()), result.getRecords().stream()
+                .map(AbnormalDeviceVO::getEquipmentId)
+                .collect(Collectors.toList()));
+    }
+
+    @Test
+    void deviceDetailIncludesAlarmAndRelatedWorkOrderSummary() {
+        doReturn(pressureDevice).when(service).getById(pressureDevice.getEquipmentId());
+        when(equipmentStatusMapper.selectOne(any())).thenReturn(status(pressureDevice.getEquipmentId(), 1, 1, 1));
+        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(
+                alarm(pressureDevice.getEquipmentCode(), "供水压力超限", 1)));
+        when(workOrderMapper.selectList(any())).thenReturn(Collections.singletonList(
+                workOrder(pressureDevice.getEquipmentId(), "WO-DETAIL", 2, "1")));
+
+        com.zksy.base.domain.vo.DeviceDetailVO detail = service.getDeviceDetail(pressureDevice.getEquipmentId());
+
+        assertEquals(1, detail.getAlertLevel());
+        assertEquals("供水压力超限", detail.getAbnormalReason());
+        assertEquals("WO-DETAIL", detail.getRelatedWorkOrderNo());
+        assertEquals(2, detail.getRelatedWorkOrderStatus());
+        assertEquals("1", detail.getRelatedWorkOrderPerson());
+    }
+
+    @Test
+    void deviceDetailKeepsMaintenanceStateSeparateFromAlarmLevel() {
+        doReturn(repairDevice).when(service).getById(repairDevice.getEquipmentId());
+        when(equipmentStatusMapper.selectOne(any())).thenReturn(
+                status(repairDevice.getEquipmentId(), 3, 0, 1));
+        when(alarmDataMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(workOrderMapper.selectList(any())).thenReturn(Collections.emptyList());
+
+        com.zksy.base.domain.vo.DeviceDetailVO detail = service.getDeviceDetail(repairDevice.getEquipmentId());
+
+        assertEquals(3, detail.getCurrentStatus());
+        assertEquals(0, detail.getAlarmStatus());
+        assertNull(detail.getAlertLevel());
+        assertEquals("设备维修中", detail.getAbnormalReason());
+    }
+
+    @Test
+    void abnormalOrderCreatesUnifiedFaultWorkOrderWithSingleReceiver() {
+        MaintainOrderVO request = new MaintainOrderVO();
+        request.setEquipmentId(pressureDevice.getEquipmentId());
+        request.setMaintainContent("压力异常");
+        request.setReceiveUserId(88L);
+        WorkOrder created = workOrder(pressureDevice.getEquipmentId(), "WO-T6", 1, "88");
+        created.setOrderId(99L);
+        when(businessWorkOrderService.createWorkOrder(any(WorkOrder.class))).thenAnswer(invocation -> {
+            WorkOrder input = invocation.getArgument(0);
+            input.setOrderId(99L);
+            input.setOrderNo("WO-T6");
+            input.setOrderStatus(1);
+            return 99L;
+        });
+        WorkOrder result = service.createAbnormalWorkOrder(request);
+
+        assertEquals(99L, result.getOrderId());
+        assertEquals(1, result.getOrderType());
+        assertEquals(2, result.getOrderLevel());
+        assertEquals(1, result.getOrderStatus());
+        assertNull(result.getReceiveUser());
+        ArgumentCaptor<WorkOrder> workOrderCaptor = ArgumentCaptor.forClass(WorkOrder.class);
+        verify(businessWorkOrderService).createWorkOrder(workOrderCaptor.capture());
+        WorkOrder submitted = workOrderCaptor.getValue();
+        assertEquals(pressureDevice.getEquipmentId(), submitted.getDeviceId());
+        assertEquals(pressureDevice.getEquipmentCode(), submitted.getDeviceCode());
+        assertEquals(1, submitted.getOrderType());
+        assertEquals(2, submitted.getOrderLevel());
+        assertEquals("压力异常", submitted.getOrderDesc());
+        assertNull(submitted.getReceiveUser());
+        verify(equipmentMaintainMapper, never()).insert(any(EquipmentMaintain.class));
+    }
+
+    @Test
+    void abnormalOrderCanCreatePendingWorkOrderWithoutReceiverId() {
+        MaintainOrderVO request = new MaintainOrderVO();
+        request.setEquipmentId(pressureDevice.getEquipmentId());
+        when(businessWorkOrderService.createWorkOrder(any(WorkOrder.class))).thenAnswer(invocation -> {
+            WorkOrder submitted = invocation.getArgument(0);
+            submitted.setOrderId(101L);
+            submitted.setOrderStatus(1);
+            return 101L;
+        });
+
+        WorkOrder result = service.createAbnormalWorkOrder(request);
+
+        assertEquals(101L, result.getOrderId());
+        assertEquals(1, result.getOrderStatus());
+        assertNull(result.getReceiveUser());
+        verify(businessWorkOrderService).createWorkOrder(any(WorkOrder.class));
+    }
+
+    @Test
+    void deviceDetailIgnoresProcessedAlarmRecordWhenEquipmentIsNormal() {
+        doReturn(repairDevice).when(service).getById(repairDevice.getEquipmentId());
+        when(equipmentStatusMapper.selectOne(any())).thenReturn(
+                status(repairDevice.getEquipmentId(), 1, 0, 1));
+        AlarmData processedAlarm = new AlarmData();
+        processedAlarm.setDeviceCode(repairDevice.getEquipmentCode());
+        processedAlarm.setAlarmStatus(0);
+        processedAlarm.setAlarmLevel(1);
+        processedAlarm.setWarningType("历史压力超限");
+        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(processedAlarm));
+        when(workOrderMapper.selectList(any())).thenReturn(Collections.emptyList());
+
+        com.zksy.base.domain.vo.DeviceDetailVO detail = service.getDeviceDetail(repairDevice.getEquipmentId());
+
+        assertEquals(0, detail.getAlarmStatus());
+        assertNull(detail.getAlertLevel());
+        assertNull(detail.getAbnormalReason());
+    }
+
+    @Test
+    void noWaterDevicesReturnsEmptyAbnormalPageWithoutReadingStatuses() {
+        when(equipmentBaseService.findByTopLevelType("供水")).thenReturn(Collections.emptyList());
+        Page<AbnormalDeviceVO> result = service.getAbnormalDevicePage(
+                1, 10, null, null, null, null);
+
+        assertEquals(0, result.getTotal());
+        assertTrue(result.getRecords().isEmpty());
+        verify(equipmentStatusMapper, never()).selectList(any());
+    }
+
+    @Test
+    void deviceDetailDoesNotExposeNonWaterDevices() {
+        doReturn(gasDevice).when(service).getById(gasDevice.getEquipmentId());
+
+        assertNull(service.getDeviceDetail(gasDevice.getEquipmentId()));
+    }
+
+    @Test
+    void createMaintainOrderRejectsNonWaterDevices() {
+        MaintainOrderVO order = new MaintainOrderVO();
+        order.setEquipmentId(gasDevice.getEquipmentId());
+        doReturn(gasDevice).when(service).getById(gasDevice.getEquipmentId());
+
+        RuntimeException error = assertThrows(RuntimeException.class,
+                () -> service.createMaintainOrder(order));
+
+        assertEquals("仅支持供水设备派发维修工单", error.getMessage());
+        verify(equipmentMaintainMapper, never()).insert(any(EquipmentMaintain.class));
+    }
+
+    @Test
+    void legacyMaintainOrderSignatureUsesUnifiedWorkOrderLifecycle() {
+        MaintainOrderVO request = new MaintainOrderVO();
+        request.setEquipmentId(pressureDevice.getEquipmentId());
+        request.setMaintainContent("压力异常");
+        request.setMaintainRemark("请尽快处理");
+        request.setReceiveUserId(88L);
+        when(businessWorkOrderService.createWorkOrder(any(WorkOrder.class))).thenAnswer(invocation -> {
+            WorkOrder submitted = invocation.getArgument(0);
+            submitted.setOrderId(100L);
+            submitted.setOrderStatus(1);
+            return 100L;
+        });
+        when(businessWorkOrderService.dispatchOrder(100L, 88L, "请尽快处理", null, null))
+                .thenReturn(true);
+
+        assertTrue(service.createMaintainOrder(request));
+
+        verify(equipmentMaintainMapper, never()).insert(any(EquipmentMaintain.class));
+        verify(businessWorkOrderService).createWorkOrder(any(WorkOrder.class));
+        verify(businessWorkOrderService).dispatchOrder(100L, 88L, "请尽快处理", null, null);
+    }
+
+    @Test
+    void maintainRecordListDoesNotExposeNonWaterDevices() {
+        EquipmentMaintain gasRecord = new EquipmentMaintain();
+        gasRecord.setEquipmentId(gasDevice.getEquipmentId());
+        gasRecord.setMaintainId("gas-record");
+        when(equipmentMaintainMapper.selectList(any())).thenReturn(Collections.singletonList(gasRecord));
+
+        assertTrue(service.getMaintainRecordList(gasDevice.getEquipmentId()).isEmpty());
+    }
+
+    @Test
+    void maintainRecordPageReturnsOnlyWaterDevices() {
+        EquipmentMaintain waterRecord = maintain("water-record", pressureDevice.getEquipmentId());
+        EquipmentMaintain gasRecord = maintain("gas-record", gasDevice.getEquipmentId());
+        Page<EquipmentMaintain> mapperPage = new Page<>(1, 10, 2);
+        mapperPage.setRecords(Arrays.asList(gasRecord, waterRecord));
+        when(equipmentMaintainMapper.selectPage(any(), any())).thenReturn(mapperPage);
+        when(equipmentMaintainMapper.selectList(any())).thenReturn(Arrays.asList(gasRecord, waterRecord));
+        doReturn(Arrays.asList(pressureDevice, gasDevice)).when(service).listByIds(any());
+
+        Page<com.zksy.base.domain.vo.EquipmentMaintainDataVO> result =
+                service.getMaintainRecordPage(1, 10, null, null, null, null, null, null);
+
+        assertEquals(1, result.getTotal());
+        assertEquals(Collections.singletonList("water-record"), result.getRecords().stream()
+                .map(com.zksy.base.domain.vo.EquipmentMaintainDataVO::getMaintainId)
+                .collect(Collectors.toList()));
+    }
+
+    @Test
+    void myMaintainRecordPageReturnsOnlyWaterDevices() {
+        EquipmentMaintain waterRecord = maintain("water-record", pressureDevice.getEquipmentId());
+        EquipmentMaintain gasRecord = maintain("gas-record", gasDevice.getEquipmentId());
+        Page<EquipmentMaintain> mapperPage = new Page<>(1, 10, 2);
+        mapperPage.setRecords(Arrays.asList(gasRecord, waterRecord));
+        when(equipmentMaintainMapper.selectPage(any(), any())).thenReturn(mapperPage);
+        when(equipmentMaintainMapper.selectList(any())).thenReturn(Arrays.asList(gasRecord, waterRecord));
+        doReturn(Arrays.asList(pressureDevice, gasDevice)).when(service).listByIds(any());
+
+        Page<com.zksy.base.domain.vo.EquipmentMaintainDataVO> result =
+                service.getMyMaintainRecordPage(1, 10, "张三", null, null, null, null);
+
+        assertEquals(1, result.getTotal());
+        assertEquals(Collections.singletonList("water-record"), result.getRecords().stream()
+                .map(com.zksy.base.domain.vo.EquipmentMaintainDataVO::getMaintainId)
+                .collect(Collectors.toList()));
+    }
+
+    @Test
+    void updateMaintainRecordRejectsNonWaterRecords() {
+        com.zksy.base.domain.vo.EquipmentMaintainDataVO vo =
+                new com.zksy.base.domain.vo.EquipmentMaintainDataVO();
+        vo.setMaintainId("gas-record");
+        when(equipmentMaintainMapper.selectById("gas-record"))
+                .thenReturn(maintain("gas-record", gasDevice.getEquipmentId()));
+
+        RuntimeException error = assertThrows(RuntimeException.class,
+                () -> service.updateMaintainRecord(vo));
+
+        assertEquals("仅支持供水设备维护记录", error.getMessage());
+        verify(equipmentMaintainMapper, never()).updateById(any(EquipmentMaintain.class));
+    }
+
+    @Test
+    void deleteMaintainRecordRejectsNonWaterRecords() {
+        when(equipmentMaintainMapper.selectById("gas-record"))
+                .thenReturn(maintain("gas-record", gasDevice.getEquipmentId()));
+
+        RuntimeException error = assertThrows(RuntimeException.class,
+                () -> service.deleteMaintainRecord("gas-record"));
+
+        assertEquals("仅支持供水设备维护记录", error.getMessage());
+        verify(equipmentMaintainMapper, never()).deleteById("gas-record");
+    }
+
+    @Test
+    void returnsEmptyRecordsWhenRequestedPageIsBeyondAbnormalDevices() {
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
+                status(pressureDevice.getEquipmentId(), 1, 1, 1)));
+        Page<AbnormalDeviceVO> result = assertDoesNotThrow(() -> service.getAbnormalDevicePage(
+                2, 10, null, null, null, null));
+
+        assertEquals(1, result.getTotal());
+        assertEquals(0, result.getRecords().size());
+    }
+
+    private static EquipmentBase device(String id, String code, String name, String typeId) {
+        EquipmentBase device = new EquipmentBase();
+        device.setEquipmentId(id);
+        device.setEquipmentCode(code);
+        device.setEquipmentName(name);
+        device.setEquipmentTypeId(typeId);
+        device.setEquipmentLocation("测试位置");
+        return device;
+    }
+
+    private static EquipmentType type(String id, String name, String parentId) {
+        EquipmentType type = new EquipmentType();
+        type.setId(id);
+        type.setTypeId(id);
+        type.setTypeName(name);
+        type.setParentTypeId(parentId);
+        return type;
+    }
+
+    private static EquipmentStatus status(String equipmentId, int currentStatus,
+                                          int alarmStatus, int onlineStatus) {
+        EquipmentStatus status = new EquipmentStatus();
+        status.setEquipmentId(equipmentId);
+        status.setCurrentStatus(currentStatus);
+        status.setAlarmStatus(alarmStatus);
+        status.setOnlineStatus(onlineStatus);
+        status.setStatusUpdateTime(LocalDateTime.of(2026, 9, 2, 10, 0));
+        return status;
+    }
+
+    private static AlarmData alarm(String deviceCode, String warningType, int level) {
+        AlarmData alarm = new AlarmData();
+        alarm.setDeviceCode(deviceCode);
+        alarm.setWarningType(warningType);
+        alarm.setAlarmLevel(level);
+        alarm.setAlarmStatus(0);
+        alarm.setAlarmTime(LocalDateTime.of(2026, 9, 2, 9, 0));
+        return alarm;
+    }
+
+    private static WorkOrder workOrder(String deviceId, String orderNo, int orderStatus, String receiveUser) {
+        WorkOrder order = new WorkOrder();
+        order.setDeviceId(deviceId);
+        order.setOrderNo(orderNo);
+        order.setOrderType(1);
+        order.setOrderStatus(orderStatus);
+        order.setReceiveUser(receiveUser);
+        return order;
+    }
+
+    private static WorkOrder workOrder(String deviceId, String orderNo, int orderStatus) {
+        return workOrder(deviceId, orderNo, orderStatus, null);
+    }
+
+
+    private static SysUser sysUser(Long userId, String userName, String nickName) {
+        SysUser user = new SysUser();
+        user.setUserId(userId);
+        user.setUserName(userName);
+        user.setNickName(nickName);
+        return user;
+    }
+
+    private static EquipmentMaintain maintain(String maintainId, String equipmentId) {
+        EquipmentMaintain record = new EquipmentMaintain();
+        record.setMaintainId(maintainId);
+        record.setEquipmentId(equipmentId);
+        record.setMaintainType("故障维修");
+        record.setMaintainPerson("张三");
+        return record;
+    }
+
+}