Parcourir la source

fix:bug修改

null il y a 2 semaines
Parent
commit
b12ecd4db1
28 fichiers modifiés avec 2189 ajouts et 60 suppressions
  1. 12 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/base/alarm/WarningThresholdController.java
  2. 65 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/basic/MonitorPointController.java
  3. 81 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/basic/PipeNetworkController.java
  4. 13 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/MaintenanceDataStatController.java
  5. 317 2
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/ManholeDeviceController.java
  6. 46 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/ManholeLayerController.java
  7. 7 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/WarningThresholdService.java
  8. 45 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/WarningThresholdServiceImpl.java
  9. 111 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillAlarmSyncService.java
  10. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillSimulationService.java
  11. 103 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/impl/DrillSimulationServiceImpl.java
  12. 22 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/EquipmentBaseService.java
  13. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/MaintenanceDataStatService.java
  14. 27 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/ManholeLayerService.java
  15. 388 45
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/EquipmentBaseServiceImpl.java
  16. 429 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/MaintenanceDataStatServiceImpl.java
  17. 235 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/ManholeLayerServiceImpl.java
  18. 46 6
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WorkOrderServiceImpl.java
  19. 21 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/AlarmMonitorListInDTO.java
  20. 34 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/MaintenanceDataStatDashboardInDTO.java
  21. 9 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/ManholeDeviceListInDTO.java
  22. 24 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/ManholeDeviceStatisticsInDTO.java
  23. 3 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/WorkOrderPageInDTO.java
  24. 26 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/AlarmMonitorListOutDTO.java
  25. 7 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/EquipmentBaseOutDTO.java
  26. 40 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/MaintenanceDataStatDashboardOutDTO.java
  27. 24 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/ManholeDataOutDTO.java
  28. 37 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/ManholeLayerStatisticsOutDTO.java

+ 12 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/base/alarm/WarningThresholdController.java

@@ -36,6 +36,18 @@ public class WarningThresholdController {
                                  @ApiParam(value = "预警编码", required = false) String warningCode){
         return AjaxResult.success(service.findByPage(pageNum, pageSize, deviceCode,warningType,warningCode));
     }
+
+    @GetMapping("/findByModulePage")
+    @ApiOperation(value = "按设备模块(燃气/排水/供水)分页查询该模块所有设备的预警阈值")
+    @Anonymous
+    public AjaxResult findByModulePage(@ApiParam(value = "页码", required = true)long pageNum,
+                                       @ApiParam(value = "页数", required = true)long pageSize,
+                                       @ApiParam(value = "设备模块类型名称,如:燃气、排水、供水", required = true) String typeName,
+                                       @ApiParam(value = "设备编码,模块内检索单个设备", required = false)String deviceCode,
+                                       @ApiParam(value = "预警类型", allowableValues = "温度预警,压力预警,湿度预警", required = false) String warningType,
+                                       @ApiParam(value = "预警编码", required = false) String warningCode){
+        return AjaxResult.success(service.findByModulePage(pageNum, pageSize, typeName, deviceCode, warningType, warningCode));
+    }
     @GetMapping("/getWarningThresholdList")
     @ApiOperation(value = "预警阈值信息查询")
     @Anonymous

+ 65 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/basic/MonitorPointController.java

@@ -0,0 +1,65 @@
+package com.zksy.web.controller.basic;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.domain.MonitorPoint;
+import com.zksy.base.dto.MonitorPointPageInDTO;
+import com.zksy.base.service.MonitorPointService;
+import com.zksy.common.annotation.Anonymous;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/monitor-point")
+@Api(tags = "监测点管理")
+public class MonitorPointController {
+
+    @Autowired
+    private MonitorPointService service;
+
+    @GetMapping("/findByPage")
+    @ApiOperation("监测点分页查询")
+    @Anonymous
+    public AjaxResult findByPage(MonitorPointPageInDTO dto) {
+        Page<MonitorPoint> page = service.queryByPage(dto);
+        return AjaxResult.success(page);
+    }
+
+    @GetMapping("/getById/{id}")
+    @ApiOperation("根据ID查询监测点")
+    @Anonymous
+    public AjaxResult getById(@PathVariable String id) {
+        MonitorPoint entity = service.getById(id);
+        return entity != null ? AjaxResult.success(entity) : AjaxResult.error("监测点信息不存在");
+    }
+
+    @PostMapping("/save")
+    @ApiOperation("新增监测点")
+    @Log(title = "新增监测点", businessType = BusinessType.INSERT)
+    public AjaxResult save(@RequestBody MonitorPoint entity) {
+        boolean saved = service.save(entity);
+        return saved ? AjaxResult.success("新增成功", saved) : AjaxResult.error("新增失败");
+    }
+
+    @PutMapping("/updateById")
+    @ApiOperation("修改监测点")
+    @Log(title = "修改监测点", businessType = BusinessType.UPDATE)
+    public AjaxResult updateById(@RequestBody MonitorPoint entity) {
+        boolean updated = service.updateById(entity);
+        return updated ? AjaxResult.success("修改成功", updated) : AjaxResult.error("修改失败");
+    }
+
+    @DeleteMapping("/deleteById")
+    @ApiOperation("删除监测点")
+    @Log(title = "删除监测点", businessType = BusinessType.DELETE)
+    public AjaxResult deleteById(@RequestParam String id) {
+        boolean deleted = service.removeById(id);
+        return deleted ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
+    }
+}

+ 81 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/basic/PipeNetworkController.java

@@ -0,0 +1,81 @@
+package com.zksy.web.controller.basic;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.domain.PipeNetworkBase;
+import com.zksy.base.dto.PipeNetworkPageInDTO;
+import com.zksy.base.service.PipeNetworkBaseService;
+import com.zksy.common.annotation.Anonymous;
+import com.zksy.common.annotation.Log;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.enums.BusinessType;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/pipe-network")
+@Api(tags = "燃气管网基础数据")
+public class PipeNetworkController {
+
+    @Autowired
+    private PipeNetworkBaseService service;
+
+    @GetMapping("/findByPage")
+    @ApiOperation("管网分页查询")
+    @Anonymous
+    public AjaxResult findByPage(PipeNetworkPageInDTO dto) {
+        Page<PipeNetworkBase> page = service.queryByPage(dto);
+        return AjaxResult.success(page);
+    }
+
+    @GetMapping("/getById/{id}")
+    @ApiOperation("根据ID查询管网")
+    @Anonymous
+    public AjaxResult getById(@PathVariable String id) {
+        PipeNetworkBase entity = service.getById(id);
+        return entity != null ? AjaxResult.success(entity) : AjaxResult.error("管网信息不存在");
+    }
+
+    @GetMapping("/options")
+    @ApiOperation("管网下拉选项")
+    @Anonymous
+    public AjaxResult options() {
+        return AjaxResult.success(service.getOptions());
+    }
+
+    @PostMapping("/save")
+    @ApiOperation("新增管网")
+    @Log(title = "新增管网基础数据", businessType = BusinessType.INSERT)
+    public AjaxResult save(@RequestBody PipeNetworkBase entity) {
+        boolean saved = service.save(entity);
+        return saved ? AjaxResult.success("新增成功", saved) : AjaxResult.error("新增失败");
+    }
+
+    @PutMapping("/updateById")
+    @ApiOperation("修改管网")
+    @Log(title = "修改管网基础数据", businessType = BusinessType.UPDATE)
+    public AjaxResult updateById(@RequestBody PipeNetworkBase entity) {
+        boolean updated = service.updateById(entity);
+        return updated ? AjaxResult.success("修改成功", updated) : AjaxResult.error("修改失败");
+    }
+
+    @DeleteMapping("/deleteById")
+    @ApiOperation("删除管网")
+    @Log(title = "删除管网基础数据", businessType = BusinessType.DELETE)
+    public AjaxResult deleteById(@RequestParam String id) {
+        boolean deleted = service.removeById(id);
+        return deleted ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
+    }
+
+    @GetMapping("/export")
+    @ApiOperation("导出Excel")
+    @Anonymous
+    public AjaxResult exportData(@RequestParam(required = false) String networkName,
+                                  @RequestParam(required = false) String status) {
+        // TODO: 实现Excel导出
+        return AjaxResult.success("导出功能待实现");
+    }
+}

+ 13 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/MaintenanceDataStatController.java

@@ -5,10 +5,12 @@ import com.zksy.base.service.MaintenanceDataStatService;
 import com.zksy.common.annotation.Anonymous;
 import com.zksy.common.core.controller.BaseController;
 import com.zksy.common.core.domain.AjaxResult2;
+import com.zksy.manhole.dto.in.MaintenanceDataStatDashboardInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatQueryInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatTrendInDTO;
 import com.zksy.manhole.dto.out.AbnormalTerminalTrendOutDTO;
 import com.zksy.manhole.dto.out.CaseRateTrendOutDTO;
+import com.zksy.manhole.dto.out.MaintenanceDataStatDashboardOutDTO;
 import com.zksy.manhole.dto.out.MaintenanceDataStatOutDTO;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -70,4 +72,15 @@ public class MaintenanceDataStatController extends BaseController
     {
         return AjaxResult2.success(maintenanceDataStatService.caseRateTrend(inDTO));
     }
+
+    /**
+     * 运维看板统计数据:一次返回KPI指标、异常终端趋势、处置率/延期率趋势、问题类型分布、区域排行、异常终端列表
+     */
+    @Anonymous
+    @PostMapping(value = "/getDashboardStat")
+    @ApiOperation(value = "运维看板统计数据")
+    public AjaxResult2<MaintenanceDataStatDashboardOutDTO> getDashboardStat(@RequestBody MaintenanceDataStatDashboardInDTO inDTO)
+    {
+        return AjaxResult2.success(maintenanceDataStatService.getDashboardStat(inDTO));
+    }
 }

+ 317 - 2
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/ManholeDeviceController.java

@@ -10,8 +10,15 @@ import com.zksy.base.alarm.service.WarningThresholdService;
 import com.zksy.base.domain.EquipmentBase;
 import com.zksy.base.domain.EquipmentStatus;
 import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.domain.ManholeDeviceRel;
+import com.zksy.base.environment.domain.ERealTimeData;
+import com.zksy.base.environment.mapper.ERealTimeDataMapper;
 import com.zksy.base.manhole.domain.ManholeData;
+import com.zksy.base.radar.domain.RadarData;
+import com.zksy.base.radar.mapper.RadarDataMapper;
 import com.zksy.base.manhole.service.ManholeDataService;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.ManholeDeviceRelMapper;
 import com.zksy.base.service.EquipmentBaseService;
 import com.zksy.base.service.EquipmentStatusService;
 import com.zksy.base.service.EquipmentTypeService;
@@ -30,7 +37,11 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletResponse;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * 窨井盖监测系统-监测设备信息管理Controller
@@ -58,6 +69,18 @@ public class ManholeDeviceController extends BaseController
     @Autowired
     private WarningThresholdService warningThresholdService;
 
+    @Autowired
+    private ManholeDeviceRelMapper manholeDeviceRelMapper;
+
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+
+    @Autowired
+    private ERealTimeDataMapper eRealTimeDataMapper;
+
+    @Autowired
+    private RadarDataMapper radarDataMapper;
+
     /**
      * 分页查询监测设备台账列表
      */
@@ -70,16 +93,50 @@ public class ManholeDeviceController extends BaseController
     }
 
     /**
-     * 查询井盖监测设备列表
+     * 窨井设备统计:区域分布(数量/占比)+ 类型分布(数量/占比)
+     */
+    @Anonymous
+    @PostMapping(value = "/statistics")
+    @ApiOperation(value = "窨井设备统计(区域分布/类型分布)")
+    public AjaxResult2<Map<String, Object>> statistics(@RequestBody ManholeDeviceStatisticsInDTO inDTO)
+    {
+        return AjaxResult2.success(equipmentBaseService.getManholeDeviceStatistics(inDTO));
+    }
+
+    /**
+     * 【态势图层专用】查询井盖监测设备列表(返回全量设备,前端按状态筛选)
+     * 【重要注释】本接口仅查询窨井类型设备:
+     *   - 排水窨井 (equipment_type_id = 9)
+     *   - 供水窨井 (equipment_type_id = 10)
+     * 不包含窨井下面的子设备,仅查询顶级窨井设备
+     * 【报警级别映射】alarmLevel: 0-有报警,1-严重报警,2-一般报警,不传则返回全部
+     * 【告警判断】通过 alarm_data 表 alarm_level 字段:
+     *   - alarm_level = 1 → 严重报警
+     *   - alarm_level > 1 → 一般报警
+     * 【查询内容】运维状态(currentStatus)、报警状态(alarmStatus)、在线状态(onlineStatus)均从设备状态表关联查询
      */
     @Anonymous
     @PostMapping(value = "/getManholeDataList")
     @ApiOperation(value = "查询井盖监测设备列表")
-    public AjaxResult2<List<EquipmentBaseOutDTO>> getManholeDataList(@RequestBody ManholeDeviceListInDTO listInDTO)
+    public AjaxResult2<Page<EquipmentBaseOutDTO>> getManholeDataList(@RequestBody ManholeDeviceListInDTO listInDTO)
     {
         return AjaxResult2.success(equipmentBaseService.getManholeDataList(listInDTO));
     }
 
+    /**
+     * 窨井盖实时监测-设备报警监控列表
+     * 查询所有窨井类型设备(排水=9/供水=10)的 jg_device_data 最新一条记录
+     * 根据倾斜角度(angle_alarm_threshold)、水浸(water_infiltration_alarm_status)、水位(water_level_alarm_status)判定严重告警
+     * 低电量(battery<=20)/弱信号(signal<=20)判定一般告警
+     */
+    @Anonymous
+    @PostMapping(value = "/getAlarmMonitorList")
+    @ApiOperation(value = "窨井盖实时监测-设备报警监控列表")
+    public AjaxResult2<AlarmMonitorListOutDTO> getAlarmMonitorList(@RequestBody AlarmMonitorListInDTO inDTO)
+    {
+        return AjaxResult2.success(equipmentBaseService.getAlarmMonitorList(inDTO));
+    }
+
     /**
      * 获取监测设备台账详情
      */
@@ -158,6 +215,264 @@ public class ManholeDeviceController extends BaseController
                 : AjaxResult.error("保存失败",result);
     }
 
+    /**
+     * 搜索可关联设备(排除窨井类型5/9/10,排除已关联设备ID)
+     */
+    @Anonymous
+    @GetMapping(value = "/rel/search")
+    @ApiOperation(value = "搜索可关联设备")
+    public AjaxResult2<List<Map<String, Object>>> searchRelDevices(
+            @RequestParam(value = "keyword", required = false) String keyword,
+            @RequestParam(value = "excludeIds", required = false) String excludeIds)
+    {
+        LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
+        // 排除窨井类型设备(5=供水窨井, 9=排水窨井, 10=燃气窨井)
+        queryWrapper.notIn(EquipmentBase::getEquipmentTypeId, new String[]{"5", "9", "10"});
+        if (StringUtils.isNotBlank(keyword)) {
+            queryWrapper.and(w -> w
+                    .like(EquipmentBase::getEquipmentCode, keyword)
+                    .or()
+                    .like(EquipmentBase::getEquipmentName, keyword)
+                    .or()
+                    .like(EquipmentBase::getEquipmentLocation, keyword));
+        }
+        // 排除已关联的设备ID
+        if (StringUtils.isNotEmpty(excludeIds)) {
+            List<String> idList = java.util.Arrays.asList(excludeIds.split(","));
+            queryWrapper.notIn(EquipmentBase::getEquipmentId, idList);
+        }
+        queryWrapper.last("LIMIT 50");
+        List<EquipmentBase> equipmentList = equipmentBaseMapper.selectList(queryWrapper);
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (EquipmentBase eq : equipmentList) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("equipmentId", eq.getEquipmentId());
+            item.put("equipmentCode", eq.getEquipmentCode());
+            item.put("equipmentName", eq.getEquipmentName());
+            item.put("equipmentLocation", eq.getEquipmentLocation());
+            item.put("equipmentTypeId", eq.getEquipmentTypeId());
+            result.add(item);
+        }
+        return AjaxResult2.success(result);
+    }
+
+    /**
+     * 查询设备已关联的井下设备列表
+     */
+    @Anonymous
+    @GetMapping(value = "/rel/list/{equipmentId}")
+    @ApiOperation(value = "查询设备已关联的井下设备列表")
+    public AjaxResult2<List<Map<String, Object>>> getRelList(@PathVariable("equipmentId") String equipmentId)
+    {
+        // 查询关联记录(manholeId 存储主设备ID)
+        List<ManholeDeviceRel> relList = manholeDeviceRelMapper.selectList(
+                new LambdaQueryWrapper<ManholeDeviceRel>()
+                        .eq(ManholeDeviceRel::getManholeId, equipmentId));
+        if (CollUtil.isEmpty(relList)) {
+            return AjaxResult2.success(new ArrayList<>());
+        }
+        // 批量查询关联设备详情
+        List<String> relEquipmentIds = relList.stream()
+                .map(ManholeDeviceRel::getEquipmentId)
+                .collect(Collectors.toList());
+        List<EquipmentBase> equipmentList = equipmentBaseMapper.selectBatchIds(relEquipmentIds);
+        Map<String, EquipmentBase> equipmentMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(equipmentList)) {
+            equipmentMap = equipmentList.stream()
+                    .collect(Collectors.toMap(EquipmentBase::getEquipmentId, e -> e, (a, b) -> a));
+        }
+        // 组装结果
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (ManholeDeviceRel rel : relList) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("relId", rel.getRelId());
+            item.put("equipmentId", rel.getEquipmentId());
+            EquipmentBase eq = equipmentMap.get(rel.getEquipmentId());
+            if (eq != null) {
+                item.put("equipmentCode", eq.getEquipmentCode());
+                item.put("equipmentName", eq.getEquipmentName());
+                item.put("equipmentLocation", eq.getEquipmentLocation());
+                item.put("equipmentTypeId", eq.getEquipmentTypeId());
+            }
+            item.put("createTime", rel.getCreateTime());
+            result.add(item);
+        }
+        return AjaxResult2.success(result);
+    }
+
+    /**
+     * 新增窨井井下设备关联
+     */
+    @Anonymous
+    @PostMapping(value = "/rel/save")
+    @ApiOperation(value = "新增窨井井下设备关联")
+    public AjaxResult saveRel(@RequestBody Map<String, String> params)
+    {
+        String manholeId = params.get("manholeId");
+        String equipmentId = params.get("equipmentId");
+        if (StringUtils.isEmpty(manholeId) || StringUtils.isEmpty(equipmentId)) {
+            return AjaxResult.error("主设备ID和关联设备ID不能为空");
+        }
+        if (manholeId.equals(equipmentId)) {
+            return AjaxResult.error("不能关联自身");
+        }
+        // 检查是否已存在关联
+        Long count = manholeDeviceRelMapper.selectCount(
+                new LambdaQueryWrapper<ManholeDeviceRel>()
+                        .eq(ManholeDeviceRel::getManholeId, manholeId)
+                        .eq(ManholeDeviceRel::getEquipmentId, equipmentId));
+        if (count > 0) {
+            return AjaxResult.error("该设备已关联,请勿重复关联");
+        }
+        // 查询设备名称和类型(冗余存储)
+        EquipmentBase equipment = equipmentBaseMapper.selectById(equipmentId);
+        ManholeDeviceRel rel = new ManholeDeviceRel();
+        rel.setManholeId(manholeId);
+        rel.setEquipmentId(equipmentId);
+        if (equipment != null) {
+            rel.setEquipmentName(equipment.getEquipmentName());
+            rel.setEquipmentType(equipment.getEquipmentTypeId());
+        }
+        rel.setCreateTime(LocalDateTime.now());
+        boolean result = manholeDeviceRelMapper.insert(rel) > 0;
+        return result ? AjaxResult.success("关联成功", result)
+                : AjaxResult.error("关联失败", result);
+    }
+
+    /**
+     * 删除窨井井下设备关联
+     */
+    @Anonymous
+    @DeleteMapping(value = "/rel/delete")
+    @ApiOperation(value = "删除窨井井下设备关联")
+    public AjaxResult deleteRel(@RequestParam("relId") Long relId)
+    {
+        if (relId == null) {
+            return AjaxResult.error("关联ID不能为空");
+        }
+        boolean result = manholeDeviceRelMapper.deleteById(relId) > 0;
+        return result ? AjaxResult.success("取消关联成功", result)
+                : AjaxResult.error("取消关联失败", result);
+    }
+
+    /**
+     * 查询设备关联的井下设备最新监测数据
+     * 根据关联设备类型查询 e_real_time_data / radar_data 等数据表
+     */
+    @Anonymous
+    @GetMapping(value = "/rel/data/{equipmentId}")
+    @ApiOperation(value = "查询设备关联的井下设备最新监测数据")
+    public AjaxResult2<List<Map<String, Object>>> getRelDeviceData(@PathVariable("equipmentId") String equipmentId)
+    {
+        // 1. 查询关联设备记录
+        List<ManholeDeviceRel> relList = manholeDeviceRelMapper.selectList(
+                new LambdaQueryWrapper<ManholeDeviceRel>()
+                        .eq(ManholeDeviceRel::getManholeId, equipmentId));
+        if (CollUtil.isEmpty(relList)) {
+            return AjaxResult2.success(new ArrayList<>());
+        }
+        // 2. 批量查询关联设备基础信息
+        List<String> relEquipmentIds = relList.stream()
+                .map(ManholeDeviceRel::getEquipmentId)
+                .collect(Collectors.toList());
+        List<EquipmentBase> equipmentList = equipmentBaseMapper.selectBatchIds(relEquipmentIds);
+        Map<String, EquipmentBase> equipmentMap = CollUtil.isNotEmpty(equipmentList)
+                ? equipmentList.stream().collect(Collectors.toMap(EquipmentBase::getEquipmentId, e -> e, (a, b) -> a))
+                : new HashMap<>();
+        // 3. 批量查询设备类型名称
+        List<String> typeIds = equipmentList.stream()
+                .map(EquipmentBase::getEquipmentTypeId)
+                .filter(StringUtils::isNotEmpty)
+                .distinct()
+                .collect(Collectors.toList());
+        Map<String, EquipmentType> typeMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(typeIds)) {
+            List<EquipmentType> typeList = equipmentTypeService.list(
+                    new LambdaQueryWrapper<EquipmentType>().in(EquipmentType::getId, typeIds));
+            if (CollUtil.isNotEmpty(typeList)) {
+                typeMap = typeList.stream().collect(Collectors.toMap(EquipmentType::getId, t -> t, (a, b) -> b));
+            }
+        }
+        // 4. 逐个查询最新监测数据(按设备类型路由)
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (ManholeDeviceRel rel : relList) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("relId", rel.getRelId());
+            item.put("equipmentId", rel.getEquipmentId());
+            item.put("equipmentName", rel.getEquipmentName());
+            item.put("equipmentType", rel.getEquipmentType());
+            EquipmentBase eq = equipmentMap.get(rel.getEquipmentId());
+            if (eq != null) {
+                item.put("equipmentCode", eq.getEquipmentCode());
+                item.put("equipmentLocation", eq.getEquipmentLocation());
+                item.put("equipmentTypeId", eq.getEquipmentTypeId());
+                EquipmentType type = typeMap.get(eq.getEquipmentTypeId());
+                String typeName = type != null ? type.getTypeName() : "";
+                item.put("equipmentTypeName", typeName);
+                // 查询最新监测数据
+                Map<String, Object> monitorData = getLatestMonitorData(eq.getEquipmentCode(), typeName);
+                if (!monitorData.isEmpty()) {
+                    item.put("monitorData", monitorData);
+                }
+            }
+            result.add(item);
+        }
+        return AjaxResult2.success(result);
+    }
+
+    /**
+     * 根据设备类型查询最新监测数据(雷达流量计/环境监测等)
+     */
+    private Map<String, Object> getLatestMonitorData(String deviceCode, String typeName) {
+        Map<String, Object> result = new HashMap<>();
+        if (StringUtils.isEmpty(deviceCode) || StringUtils.isEmpty(typeName)) {
+            return result;
+        }
+        try {
+            if (typeName.contains("雷达") || typeName.contains("流量")) {
+                RadarData data = radarDataMapper.selectOne(
+                        new LambdaQueryWrapper<RadarData>()
+                                .eq(RadarData::getDeviceCode, deviceCode)
+                                .orderByDesc(RadarData::getCreateTime)
+                                .last("LIMIT 1"));
+                if (data != null) {
+                    result.put("instantFlow", data.getMeter1InstantFlow());
+                    result.put("flowSpeed", data.getFlowSpeed());
+                    result.put("waterLevel", data.getWaterLevel());
+                    result.put("totalFlow", data.getMeter1NetTotal());
+                    result.put("signalQuality", data.getSignalQuality());
+                    result.put("createTime", data.getCreateTime());
+                    result.put("dataType", "雷达流量计");
+                }
+            } else if (typeName.contains("环境") || typeName.contains("水质")) {
+                Integer devId = Integer.parseInt(deviceCode);
+                ERealTimeData data = eRealTimeDataMapper.selectOne(
+                        new LambdaQueryWrapper<ERealTimeData>()
+                                .eq(ERealTimeData::getDeviceId, devId)
+                                .orderByDesc(ERealTimeData::getCreateTime)
+                                .last("LIMIT 1"));
+                if (data != null) {
+                    result.put("temperature", data.getTem());
+                    result.put("humidity", data.getHum());
+                    result.put("createTime", data.getCreateTime());
+                    result.put("dataType", "环境监测");
+                    if (data.getNodeId() != null) {
+                        switch (data.getNodeId()) {
+                            case 1: result.put("metricName", "悬浮物"); result.put("metricValue", data.getFloatValue()); break;
+                            case 2: result.put("metricName", "COD"); result.put("metricValue", data.getHum()); break;
+                            case 3: result.put("metricName", "氨氮"); result.put("metricValue", data.getHum()); break;
+                            case 4: result.put("metricName", "电导率"); result.put("metricValue", data.getHum()); break;
+                            case 5: result.put("metricName", "PH"); result.put("metricValue", data.getHum()); break;
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // 设备编码非数字或查询异常时忽略
+        }
+        return result;
+    }
+
     /**
      * 根据设备类别分组统计设备数量
      */

+ 46 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/manhole/ManholeLayerController.java

@@ -0,0 +1,46 @@
+package com.zksy.web.controller.manhole;
+
+
+import com.zksy.base.service.ManholeLayerService;
+import com.zksy.common.annotation.Anonymous;
+import com.zksy.common.core.controller.BaseController;
+import com.zksy.common.core.domain.AjaxResult2;
+import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 窨井盖监测系统 - 态势图层 Controller
+ * 
+ * @author xianggx
+ */
+@RestController
+@RequestMapping("/manhole/layer")
+@Api(tags = "窨井盖监测系统 - 态势图层")
+public class ManholeLayerController extends BaseController {
+
+    @Autowired
+    private ManholeLayerService manholeLayerService;
+
+    /**
+     * 获取态势图层数据(设备列表 + 统计)
+     * 【重要说明】本接口仅查窨井类型设备:排水窨井 (equipment_type_id=9)/供水窨井 (equipment_type_id=10)
+     * 不包含窨井下面的子设备,仅查询顶级窨井设备
+     * 【报警级别映射】alarmLevel: 0-有报警,1-严重报警,2-一般报警
+     * 【告警判断】通过 alarm_data 表 alarm_level 字段:
+     *   - alarm_level = 1 → 严重报警
+     *   - alarm_level > 1 → 一般报警
+     * 【查询内容】运维状态、报警状态、在线状态等均从设备状态表关联查询
+     */
+    @Anonymous
+    @PostMapping(value = "/getManholeLayerData")
+    @ApiOperation(value = "获取态势图层数据")
+    public AjaxResult2<Map<String, Object>> getManholeLayerData(@RequestBody ManholeDeviceListInDTO listInDTO) {
+        return AjaxResult2.success(manholeLayerService.getManholeLayerData(listInDTO));
+    }
+
+}

+ 7 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/WarningThresholdService.java

@@ -19,4 +19,11 @@ public interface WarningThresholdService extends IService<WarningThreshold> {
      * 根据设备编号查询该设备所有预警值
      */
     WarningThreshold getWarningThresholdByDeviceAndCode(String deviceCode, String warningCode);
+
+    /**
+     * 按设备模块(燃气/排水/供水)分页查询预警阈值
+     * @param typeName 顶级设备类型名称,如:燃气、排水、供水
+     * @param deviceCode 设备编码,可选,用于在模块内精确检索单个设备
+     */
+    Page<WarningThreshold> findByModulePage(long pageNum, long pageSize, String typeName, String deviceCode, String warningType, String warningCode);
 }

+ 45 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/WarningThresholdServiceImpl.java

@@ -7,9 +7,14 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.base.alarm.domain.WarningThreshold;
 import com.zksy.base.alarm.mapper.WarningThresholdMapper;
 import com.zksy.base.alarm.service.WarningThresholdService;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.service.EquipmentBaseService;
+import cn.hutool.core.collection.CollUtil;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
 * @author Administrator
@@ -20,6 +25,9 @@ import java.util.List;
 public class WarningThresholdServiceImpl extends ServiceImpl<WarningThresholdMapper, WarningThreshold>
     implements WarningThresholdService {
 
+    @Autowired
+    private EquipmentBaseService equipmentBaseService;
+
     @Override
     public Page<WarningThreshold> findByPage(long pageNum, long pageSize, String deviceCode, String warningType, String warningCode) {
         Page<WarningThreshold> page = new Page<>(pageNum, pageSize);
@@ -33,6 +41,43 @@ public class WarningThresholdServiceImpl extends ServiceImpl<WarningThresholdMap
         return this.page(page, queryWrapper);
     }
 
+    @Override
+    public Page<WarningThreshold> findByModulePage(long pageNum, long pageSize, String typeName, String deviceCode, String warningType, String warningCode) {
+        Page<WarningThreshold> page = new Page<>(pageNum, pageSize);
+        // 1. 查询该模块下所有设备编码
+        List<EquipmentBase> equipmentList = equipmentBaseService.findByTopLevelType(typeName);
+        if (CollUtil.isEmpty(equipmentList)) {
+            return page;
+        }
+        List<String> deviceCodes = equipmentList.stream()
+                .map(EquipmentBase::getEquipmentCode)
+                .filter(code -> code != null && !code.isEmpty())
+                .distinct()
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(deviceCodes)) {
+            return page;
+        }
+        // 2. 查询阈值:device_code 字段匹配任意一个模块设备编码(支持逗号分隔多设备)
+        LambdaQueryWrapper<WarningThreshold> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.and(w -> {
+            for (String code : deviceCodes) {
+                w.or().apply("CONCAT(',', device_code, ',') LIKE CONCAT('%,', {0}, ',%')", code);
+            }
+        });
+        // 3. 模块内按设备编码精确检索单个设备
+        if (deviceCode != null && !deviceCode.isEmpty()) {
+            queryWrapper.apply("CONCAT(',', device_code, ',') LIKE CONCAT('%,', {0}, ',%')", deviceCode);
+        }
+        if (warningType != null && !warningType.isEmpty()) {
+            queryWrapper.like(WarningThreshold::getWarningType, warningType);
+        }
+        if (warningCode != null && !warningCode.isEmpty()) {
+            queryWrapper.like(WarningThreshold::getWarningCode, warningCode);
+        }
+        queryWrapper.orderByDesc(WarningThreshold::getUpdateTime);
+        return this.page(page, queryWrapper);
+    }
+
     @Override
     public List<WarningThreshold> getWarningThresholdList(String deviceCode, String warningType, String warningCode) {
         LambdaQueryWrapper<WarningThreshold> queryWrapper = new LambdaQueryWrapper<>();

+ 111 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillAlarmSyncService.java

@@ -0,0 +1,111 @@
+package com.zksy.base.drill.service;
+
+import cn.hutool.core.date.DateUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.domain.CaseInfo;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.domain.WorkOrderLog;
+import com.zksy.base.mapper.CaseInfoMapper;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.WorkOrderLogMapper;
+import com.zksy.base.mapper.WorkOrderMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * 演练告警同步创建案件/工单 — 从 zk-api-service 的 AlarmDataServiceImpl.syncCreateCaseAndWorkOrder 复制
+ */
+@Slf4j
+@Service
+public class DrillAlarmSyncService {
+
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+
+    @Autowired
+    private CaseInfoMapper caseInfoMapper;
+
+    @Autowired
+    private WorkOrderMapper workOrderMapper;
+
+    @Autowired
+    private WorkOrderLogMapper workOrderLogMapper;
+
+    private static final List<String> CASE_REQUIRED_TYPE_IDS = Arrays.asList("5", "9", "10");
+
+    public void syncCreateCaseAndWorkOrder(AlarmData alarmData) {
+        EquipmentBase equipmentBase = equipmentBaseMapper.selectOne(
+                new LambdaQueryWrapper<EquipmentBase>()
+                        .eq(EquipmentBase::getEquipmentCode, alarmData.getDeviceCode())
+                        .last("LIMIT 1"));
+        if (equipmentBase == null) {
+            log.warn("未找到设备编码对应的设备信息,deviceCode={}", alarmData.getDeviceCode());
+            return;
+        }
+        String typeId = equipmentBase.getEquipmentTypeId();
+        boolean needCreateCase = CASE_REQUIRED_TYPE_IDS.contains(typeId);
+        int caseLevel = alarmData.getAlarmLevel() != null ? alarmData.getAlarmLevel() : 4;
+        int orderLevel = caseLevel <= 2 ? 1 : (caseLevel == 3 ? 2 : 3);
+        String desc = alarmData.getWarningType() != null ? alarmData.getWarningType() : "设备告警";
+        if (alarmData.getRemark() != null) {
+            desc += "-" + alarmData.getRemark();
+        }
+        CaseInfo caseInfo = null;
+        if (needCreateCase) {
+            caseInfo = new CaseInfo();
+            caseInfo.setCaseNo(generateCaseNo());
+            caseInfo.setAlarmId(alarmData.getId());
+            caseInfo.setDeviceId(equipmentBase.getEquipmentId());
+            caseInfo.setDeviceCode(alarmData.getDeviceCode());
+            caseInfo.setCaseLevel(caseLevel);
+            caseInfo.setCaseDesc(desc);
+            caseInfo.setCaseSource(1);
+            caseInfo.setCaseStatus(1);
+            caseInfoMapper.insert(caseInfo);
+            log.info("同步新增案件信息成功,caseNo={}", caseInfo.getCaseNo());
+        }
+        WorkOrder workOrder = new WorkOrder();
+        workOrder.setOrderNo(generateOrderNo());
+        workOrder.setAlarmId(alarmData.getId());
+        workOrder.setDeviceId(equipmentBase.getEquipmentId());
+        workOrder.setDeviceCode(alarmData.getDeviceCode());
+        workOrder.setOrderType(1);
+        workOrder.setOrderLevel(orderLevel);
+        workOrder.setOrderDesc(desc);
+        workOrder.setOrderStatus(1);
+        if (caseInfo != null) {
+            workOrder.setCaseId(caseInfo.getCaseId());
+            workOrder.setCaseNo(caseInfo.getCaseNo());
+        }
+        workOrderMapper.insert(workOrder);
+        log.info("同步新增工单成功,orderNo={}", workOrder.getOrderNo());
+        WorkOrderLog workOrderLog = new WorkOrderLog();
+        workOrderLog.setOrderId(workOrder.getOrderId());
+        workOrderLog.setOperType(1);
+        workOrderLog.setOperDesc(desc + ",系统告警自动创建工单");
+        workOrderLog.setOperUserName("系统");
+        workOrderLog.setOperUserId(-1L);
+        workOrderLog.setOperTime(DateUtil.date());
+        workOrderLogMapper.insert(workOrderLog);
+        log.info("同步新增工单操作日志成功,orderId={}", workOrder.getOrderId());
+    }
+
+    private static String generateCaseNo() {
+        String dt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        return "CS" + dt + String.format("%04d", new Random().nextInt(10000));
+    }
+
+    private static String generateOrderNo() {
+        String dt = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+        return "WO" + dt + String.format("%04d", new Random().nextInt(10000));
+    }
+}

+ 8 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/DrillSimulationService.java

@@ -0,0 +1,8 @@
+package com.zksy.base.drill.service;
+
+import java.util.Map;
+
+public interface DrillSimulationService {
+
+    Map<String, Object> triggerAlarm(String category);
+}

+ 103 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/drill/service/impl/DrillSimulationServiceImpl.java

@@ -0,0 +1,103 @@
+package com.zksy.base.drill.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.alarm.service.AlarmDataService;
+import com.zksy.base.drill.service.DrillAlarmSyncService;
+import com.zksy.base.drill.service.DrillSimulationService;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.EquipmentTypeMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class DrillSimulationServiceImpl implements DrillSimulationService {
+
+    @Autowired
+    private EquipmentTypeMapper equipmentTypeMapper;
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+    @Autowired
+    private AlarmDataService alarmDataService;
+    @Autowired
+    private DrillAlarmSyncService drillAlarmSyncService;
+
+    @Override
+    public Map<String, Object> triggerAlarm(String category) {
+        EquipmentType topType = equipmentTypeMapper.selectOne(
+                new LambdaQueryWrapper<EquipmentType>()
+                        .eq(EquipmentType::getTypeName, category)
+                        .eq(EquipmentType::getParentTypeId, "0"));
+        if (topType == null) {
+            throw new IllegalArgumentException("未找到项目类型对应的设备分类:" + category);
+        }
+        List<String> typeIds = new ArrayList<>();
+        typeIds.add(topType.getId());
+        collectChildTypeIds(topType.getId(), typeIds);
+
+        List<EquipmentBase> equipments = equipmentBaseMapper.selectList(
+                new LambdaQueryWrapper<EquipmentBase>()
+                        .in(EquipmentBase::getEquipmentTypeId, typeIds));
+        if (CollUtil.isEmpty(equipments)) {
+            throw new IllegalArgumentException("未找到" + category + "类型下的设备");
+        }
+        Map<String, String> typeNameMap = equipmentTypeMapper.selectBatchIds(typeIds).stream()
+                .collect(Collectors.toMap(EquipmentType::getId, EquipmentType::getTypeName, (a, b) -> a));
+
+        int successCount = 0, failCount = 0;
+        LocalDateTime now = LocalDateTime.now();
+        for (EquipmentBase eq : equipments) {
+            try {
+                AlarmData alarm = new AlarmData();
+                alarm.setDeviceCode(eq.getEquipmentCode());
+                alarm.setWarningType(typeNameMap.getOrDefault(eq.getEquipmentTypeId(), "未知类型") + "模拟告警");
+                alarm.setWarningCode("DRILL-" + System.currentTimeMillis());
+                alarm.setMinValue(new BigDecimal("0"));
+                alarm.setMaxValue(new BigDecimal("100"));
+                alarm.setActualValue(new BigDecimal("150"));
+                alarm.setAlarmLevel(1);
+                alarm.setAlarmStatus(0);
+                alarm.setAlarmTime(now);
+                alarm.setDeviationRatio(new BigDecimal("50"));
+                alarm.setRemark("演练模拟告警-" + category);
+                alarm.setCreateTime(now);
+                boolean saved = alarmDataService.saveAlarmData(alarm);
+                if (saved) {
+                    drillAlarmSyncService.syncCreateCaseAndWorkOrder(alarm);
+                    successCount++;
+                } else {
+                    failCount++;
+                }
+            } catch (Exception e) {
+                log.error("模拟告警失败, equipmentId={}", eq.getEquipmentId(), e);
+                failCount++;
+            }
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("category", category);
+        result.put("total", equipments.size());
+        result.put("success", successCount);
+        result.put("fail", failCount);
+        return result;
+    }
+
+    private void collectChildTypeIds(String parentId, List<String> typeIds) {
+        List<EquipmentType> children = equipmentTypeMapper.selectList(
+                new LambdaQueryWrapper<EquipmentType>()
+                        .eq(EquipmentType::getParentTypeId, parentId));
+        for (EquipmentType child : children) {
+            typeIds.add(child.getId());
+            collectChildTypeIds(child.getId(), typeIds);
+        }
+    }
+}

+ 22 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/EquipmentBaseService.java

@@ -4,10 +4,13 @@ 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.vo.EquipmentFullVO;
+import com.zksy.manhole.dto.in.AlarmMonitorListInDTO;
 import com.zksy.manhole.dto.in.EquipmentCountByTypeInDTO;
 import com.zksy.manhole.dto.in.EquipmentExportInDTO;
 import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
 import com.zksy.manhole.dto.in.ManholeDevicePageInDTO;
+import com.zksy.manhole.dto.in.ManholeDeviceStatisticsInDTO;
+import com.zksy.manhole.dto.out.AlarmMonitorListOutDTO;
 import com.zksy.manhole.dto.out.EquipmentBaseOutDTO;
 import com.zksy.manhole.dto.out.EquipmentCountByTypeOutDTO;
 import com.zksy.manhole.dto.out.EquipmentExportOutDTO;
@@ -53,11 +56,18 @@ public interface EquipmentBaseService extends IService<EquipmentBase> {
     Page<EquipmentBaseOutDTO> findManholeDeviceByPage(ManholeDevicePageInDTO pageInDTO);
 
     /**
-     * 查询井盖监测设备列表
+     * 查询井盖监测设备列表(仅查窨井类型设备:排水窨井=9/供水窨井=10),支持分页与报警级别过滤
      * @param listInDTO
      * @return
      */
-    List<EquipmentBaseOutDTO> getManholeDataList(ManholeDeviceListInDTO listInDTO);
+    Page<EquipmentBaseOutDTO> getManholeDataList(ManholeDeviceListInDTO listInDTO);
+
+    /**
+     * 窨井设备统计:区域分布(数量/占比)+ 类型分布(数量/占比)
+     * @param inDTO 筛选条件(区域/设备类型)
+     * @return regionDistribution + typeDistribution + total
+     */
+    Map<String, Object> getManholeDeviceStatistics(ManholeDeviceStatisticsInDTO inDTO);
 
     /**
      * 根据设备类别分组统计设备数量
@@ -79,4 +89,13 @@ public interface EquipmentBaseService extends IService<EquipmentBase> {
      * @return 设备列表
      */
     List<EquipmentBase> findByTopLevelType(String typeName);
-}
+
+    /**
+     * 窨井盖实时监测-设备报警监控列表
+     * 查询所有窨井类型(equipment_type_id IN (9,10))设备的 jg_device_data 最新一条记录
+     * 根据倾斜角度/水浸/水位判定严重告警,低电量/弱信号判定一般告警
+     * @param inDTO 查询条件(keyword 模糊匹配)
+     * @return 设备列表 + 统计信息
+     */
+    AlarmMonitorListOutDTO getAlarmMonitorList(AlarmMonitorListInDTO inDTO);
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/MaintenanceDataStatService.java

@@ -2,10 +2,12 @@ package com.zksy.base.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.zksy.base.domain.MaintenanceDataStat;
+import com.zksy.manhole.dto.in.MaintenanceDataStatDashboardInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatQueryInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatTrendInDTO;
 import com.zksy.manhole.dto.out.AbnormalTerminalTrendOutDTO;
 import com.zksy.manhole.dto.out.CaseRateTrendOutDTO;
+import com.zksy.manhole.dto.out.MaintenanceDataStatDashboardOutDTO;
 import com.zksy.manhole.dto.out.MaintenanceDataStatOutDTO;
 
 import java.util.List;
@@ -37,4 +39,11 @@ public interface MaintenanceDataStatService extends IService<MaintenanceDataStat
      * @return
      */
     List<CaseRateTrendOutDTO> caseRateTrend(MaintenanceDataStatTrendInDTO inDTO);
+
+    /**
+     * 运维看板统计数据:一次返回KPI指标、异常终端趋势、处置率/延期率趋势、问题类型分布、区域排行、异常终端列表
+     * @param inDTO
+     * @return
+     */
+    MaintenanceDataStatDashboardOutDTO getDashboardStat(MaintenanceDataStatDashboardInDTO inDTO);
 }

+ 27 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/ManholeLayerService.java

@@ -0,0 +1,27 @@
+package com.zksy.base.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
+import com.zksy.manhole.dto.out.EquipmentBaseOutDTO;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * 态势图层 - 窨井设备服务
+ * 【重要说明】此 Service 专用于态势图层,仅查窨井类型设备(9-排水窨井/10-供水窨井),不包含子设备
+ * 【报警级别】根据 alarm_data 表:alarm_level=1 严重报警,alarm_level>1 一般报警
+ */
+public interface ManholeLayerService {
+
+    /**
+     * 获取态势图层数据(设备列表 + 统计)
+     * @param listInDTO 筛选条件(keyword/currentStatus/alarmStatus/onlineStatus)
+     * @return { devices: 设备列表(含状态、坐标、最新报警),statistics: 状态统计 }
+     */
+    Map<String, Object> getManholeLayerData(ManholeDeviceListInDTO listInDTO);
+
+}

+ 388 - 45
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/EquipmentBaseServiceImpl.java

@@ -16,10 +16,12 @@ import com.zksy.base.manhole.mapper.ManholeDataMapper;
 import com.zksy.base.mapper.*;
 import com.zksy.base.service.EquipmentBaseService;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.manhole.dto.in.AlarmMonitorListInDTO;
 import com.zksy.manhole.dto.in.EquipmentCountByTypeInDTO;
 import com.zksy.manhole.dto.in.EquipmentExportInDTO;
 import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
 import com.zksy.manhole.dto.in.ManholeDevicePageInDTO;
+import com.zksy.manhole.dto.in.ManholeDeviceStatisticsInDTO;
 import com.zksy.manhole.dto.out.*;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -27,6 +29,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.function.Function;
@@ -67,6 +71,9 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
     @Autowired
     private PipeNetworkBaseMapper pipeNetworkBaseMapper;
 
+    @Autowired
+    private WorkOrderMapper workOrderMapper;
+
     @Override
     public Page<EquipmentBase> findByPage(long pageNum, long pageSize,
                                           String equipmentCode, String equipmentName,
@@ -256,7 +263,7 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
      */
     @Override
     public Page<EquipmentBaseOutDTO> findManholeDeviceByPage(ManholeDevicePageInDTO pageInDTO) {
-        log.info("分页查询监测设备台账列表-入参:{}", pageInDTO);
+        //log.info("分页查询监测设备台账列表-入参:{}", pageInDTO);
         List<String> equipmentIds = null;
         if(pageInDTO.getCurrentStatus() != null){
             List<EquipmentStatus> equipmentStatusList = equipmentStatusMapper.selectList(new LambdaQueryWrapper<EquipmentStatus>()
@@ -268,8 +275,11 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
         }
         Page<EquipmentBase> page = new Page<>(pageInDTO.getPageNum(), pageInDTO.getPageSize());
         LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.eq(StringUtils.isNotEmpty(pageInDTO.getDeviceCode()), EquipmentBase::getEquipmentCode, pageInDTO.getDeviceCode())
-                .eq(StringUtils.isNotEmpty(pageInDTO.getEquipmentTypeId()), EquipmentBase::getEquipmentTypeId, pageInDTO.getEquipmentTypeId())
+        // 窨井盖设备类型过滤:排水窨井(equipment_type_id=9)/供水窨井(equipment_type_id=10),筛选时仅查所选类别
+        List<String> manholeTypeIds = StringUtils.isNotEmpty(pageInDTO.getEquipmentTypeId())
+                ? Collections.singletonList(pageInDTO.getEquipmentTypeId()) : Arrays.asList("5", "9", "10");
+        queryWrapper.in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                .eq(StringUtils.isNotEmpty(pageInDTO.getDeviceCode()), EquipmentBase::getEquipmentCode, pageInDTO.getDeviceCode())
                 .in(CollUtil.isNotEmpty(equipmentIds), EquipmentBase::getEquipmentId, equipmentIds)
                 .like(StringUtils.isNotEmpty(pageInDTO.getDeviceName()), EquipmentBase::getEquipmentName, pageInDTO.getDeviceName())
                 .like(StringUtils.isNotEmpty(pageInDTO.getEquipmentLocation()), EquipmentBase::getEquipmentLocation, pageInDTO.getEquipmentLocation())
@@ -330,13 +340,78 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
         return outPage;
     }
 
+    /**
+     * 窨井设备统计:区域分布(数量/占比)+ 类型分布(数量/占比)
+     * @param inDTO 筛选条件(区域/设备类型)
+     * @return regionDistribution + typeDistribution + total
+     */
+    @Override
+    public Map<String, Object> getManholeDeviceStatistics(ManholeDeviceStatisticsInDTO inDTO) {
+        // 窨井设备:排水窨井(equipment_type_id=9)/供水窨井(equipment_type_id=10),筛选时仅统计所选类别
+        List<String> manholeTypeIds = StringUtils.isNotEmpty(inDTO.getEquipmentTypeId())
+                ? Collections.singletonList(inDTO.getEquipmentTypeId()) : Arrays.asList("5", "9", "10");
+        List<EquipmentBase> devices = this.list(new LambdaQueryWrapper<EquipmentBase>()
+                .in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                .eq(StringUtils.isNotEmpty(inDTO.getDistrict()), EquipmentBase::getDistrict, inDTO.getDistrict()));
+        int total = devices.size();
+
+        // 区域分布:按所属片区(district)分组统计
+        Map<String, Long> regionCountMap = devices.stream()
+                .filter(d -> StringUtils.isNotBlank(d.getDistrict()))
+                .collect(Collectors.groupingBy(EquipmentBase::getDistrict, Collectors.counting()));
+        List<Map<String, Object>> regionDistribution = regionCountMap.entrySet().stream()
+                .sorted((a, b) -> Long.compare(b.getValue(), a.getValue()))
+                .map(e -> {
+                    Map<String, Object> item = new HashMap<>();
+                    item.put("name", e.getKey());
+                    item.put("count", e.getValue());
+                    item.put("percent", calcPercent(e.getValue(), total));
+                    return item;
+                })
+                .collect(Collectors.toList());
+
+        // 类型分布:按设备类型分组统计(9=排水窨井设备,10=供水窨井设备)
+        Map<String, String> typeNameMap = new HashMap<>();
+        typeNameMap.put("9", "排水窨井设备");
+        typeNameMap.put("10", "供水窨井设备");
+        Map<String, Long> typeCountMap = devices.stream()
+                .filter(d -> StringUtils.isNotBlank(d.getEquipmentTypeId()))
+                .collect(Collectors.groupingBy(EquipmentBase::getEquipmentTypeId, Collectors.counting()));
+        List<Map<String, Object>> typeDistribution = typeCountMap.entrySet().stream()
+                .sorted((a, b) -> Long.compare(b.getValue(), a.getValue()))
+                .map(e -> {
+                    Map<String, Object> item = new HashMap<>();
+                    item.put("name", typeNameMap.getOrDefault(e.getKey(), e.getKey()));
+                    item.put("count", e.getValue());
+                    item.put("percent", calcPercent(e.getValue(), total));
+                    return item;
+                })
+                .collect(Collectors.toList());
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("total", total);
+        result.put("regionDistribution", regionDistribution);
+        result.put("typeDistribution", typeDistribution);
+        return result;
+    }
+
+    /**
+     * 计算占比(保留1位小数)
+     */
+    private BigDecimal calcPercent(Long count, int total) {
+        if (total <= 0) {
+            return BigDecimal.ZERO;
+        }
+        return new BigDecimal(count * 100.0 / total).setScale(1, RoundingMode.HALF_UP);
+    }
+
     /**
      * 查询井盖监测设备列表
      * @param listInDTO
      * @return
      */
     @Override
-    public List<EquipmentBaseOutDTO> getManholeDataList(ManholeDeviceListInDTO listInDTO) {
+    public Page<EquipmentBaseOutDTO> getManholeDataList(ManholeDeviceListInDTO listInDTO) {
         log.info("查询井盖监测设备列表-入参:{}", listInDTO);
         List<String> equipmentIds = null;
         if(listInDTO.getCurrentStatus() != null || listInDTO.getAlarmStatus() != null || listInDTO.getOnlineStatus() != null){
@@ -345,12 +420,18 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
                     .eq(listInDTO.getAlarmStatus() != null, EquipmentStatus::getAlarmStatus, listInDTO.getAlarmStatus())
                     .eq(listInDTO.getOnlineStatus() != null, EquipmentStatus::getOnlineStatus, listInDTO.getOnlineStatus()));
             if(CollUtil.isEmpty(equipmentStatusList)){
-                return new ArrayList<>();
+                return new Page<>(1, 10, 0);
             }
             equipmentIds = equipmentStatusList.stream().map(EquipmentStatus::getEquipmentId).distinct().collect(Collectors.toList());
         }
+        // 仅查询窨井类型设备:排水窨井(equipment_type_id=9)/供水窨井(equipment_type_id=10)
+        List<String> manholeTypeIds = Arrays.asList("5", "9", "10");
+        // 按报警级别过滤时需要 manholeData 参与判定,须全量查出后内存过滤+内存分页
+        boolean needLevelFilter = listInDTO.getAlarmLevel() != null;
+        boolean needManholeData = Boolean.TRUE.equals(listInDTO.getIsQueryManholeData()) || needLevelFilter;
         LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.in(CollUtil.isNotEmpty(equipmentIds), EquipmentBase::getEquipmentId, equipmentIds)
+        queryWrapper.in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                .in(CollUtil.isNotEmpty(equipmentIds), EquipmentBase::getEquipmentId, equipmentIds)
                 .and(StringUtils.isNotBlank(listInDTO.getKeyword()), wrapper -> wrapper
                         .like(EquipmentBase::getEquipmentCode, listInDTO.getKeyword())
                         .or()
@@ -358,59 +439,321 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
                         .or()
                         .like(EquipmentBase::getEquipmentLocation, listInDTO.getKeyword()))
                 .orderByDesc(EquipmentBase::getUpdateTime);
-        List<EquipmentBase> equipmentBaseList = this.list(queryWrapper);
-        if (CollUtil.isEmpty(equipmentBaseList)) {
+
+        Page<EquipmentBase> rs;
+        if (needLevelFilter) {
+            // 报警级别需基于最新监测数据判定,先全量查询再内存过滤/分页
+            List<EquipmentBase> allList = this.list(queryWrapper);
+            rs = new Page<>(1, Math.max(allList.size(), 1), allList.size());
+            rs.setRecords(allList);
+        } else if (listInDTO.getPageNum() != null && listInDTO.getPageSize() != null) {
+            Page<EquipmentBase> page = new Page<>(listInDTO.getPageNum(), listInDTO.getPageSize());
+            rs = this.page(page, queryWrapper);
+        } else {
+            List<EquipmentBase> allList = this.list(queryWrapper);
+            rs = new Page<>(1, Math.max(allList.size(), 1), allList.size());
+            rs.setRecords(allList);
+        }
+        List<EquipmentBaseOutDTO> outDTOList = BeanUtil.copyToList(rs.getRecords(), EquipmentBaseOutDTO.class);
+        if (CollUtil.isNotEmpty(outDTOList)) {
+            //查询设备类型
+            List<String> equipmentTypeIdList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentTypeId).distinct().collect(Collectors.toList());
+            LambdaQueryWrapper<EquipmentType> batchQw = new LambdaQueryWrapper<>();
+            batchQw.in(EquipmentType::getId, equipmentTypeIdList);
+            List<EquipmentType> equipmentTypeList = equipmentTypeMapper.selectList(batchQw);
+            Map<String, EquipmentType> equipmentTypeMap = new HashMap<>();
+            if(CollUtil.isNotEmpty(equipmentTypeList)){
+                equipmentTypeMap.putAll(equipmentTypeList.stream().collect(
+                    Collectors.toMap(t -> String.valueOf(t.getId()), Function.identity(), (key1, key2) -> key2)));
+            }
+            //查询设备状态
+            List<String> equipmentIdList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentId).distinct().collect(Collectors.toList());
+            List<EquipmentStatus> equipmentStatusList = equipmentStatusMapper.selectList(new LambdaQueryWrapper<EquipmentStatus>()
+                    .in(EquipmentStatus::getEquipmentId, equipmentIdList));
+            Map<String, EquipmentStatus> equipmentStatusMap = new HashMap<>();
+            if(CollUtil.isNotEmpty(equipmentStatusList)){
+                equipmentStatusMap.putAll(equipmentStatusList.stream().collect(Collectors.toMap(EquipmentStatus::getEquipmentId, Function.identity(), (key1, key2) -> key2)));
+            }
+            //查询设备监测状态
+            Map<String, List<ManholeData>> imeiCardNumberMapList = new HashMap<>();
+            if(needManholeData){
+                List<String> equipmentCodeList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentCode).distinct().collect(Collectors.toList());
+                List<ManholeData> manholeDataList = manholeDataMapper.selectList(new LambdaQueryWrapper<ManholeData>()
+                        .in(ManholeData::getImeiCardNumber, equipmentCodeList)
+                        .orderByDesc(ManholeData::getCreateTime));
+                imeiCardNumberMapList.putAll(manholeDataList.stream().collect(Collectors.groupingBy(ManholeData::getImeiCardNumber)));
+            }
+            List<ManholeData> manholeDataList = null;
+            for(EquipmentBaseOutDTO outDTO : outDTOList) {
+                EquipmentType equipmentType = equipmentTypeMap.get(outDTO.getEquipmentTypeId());
+                if(equipmentType != null){
+                    outDTO.setEquipmentTypeName(equipmentType.getTypeName());
+                }
+                EquipmentStatus equipmentStatus = equipmentStatusMap.get(outDTO.getEquipmentId());
+                if(equipmentStatus != null){
+                    EquipmentStatusOutDTO equipmentStatusOutDTO = BeanUtil.copyProperties(equipmentStatus, EquipmentStatusOutDTO.class);
+                    outDTO.setEquipmentStatus(equipmentStatusOutDTO);
+                }
+                if(needManholeData){
+                    manholeDataList = imeiCardNumberMapList.get(outDTO.getEquipmentCode());
+                    if(CollUtil.isNotEmpty(manholeDataList)){
+                        ManholeData manholeData = manholeDataList.get(0);
+                        ManholeDataOutDTO manholeDataOutDTO = BeanUtil.copyProperties(manholeData, ManholeDataOutDTO.class);
+                        outDTO.setManholeData(manholeDataOutDTO);
+                    }
+                }
+            }
+        }
+
+        long total;
+        List<EquipmentBaseOutDTO> pageRecords;
+        if (needLevelFilter) {
+            // 内存过滤报警级别
+            List<EquipmentBaseOutDTO> filteredList = outDTOList.stream()
+                    .filter(dto -> matchAlarmLevel(dto, listInDTO.getAlarmLevel()))
+                    .collect(Collectors.toList());
+            total = filteredList.size();
+            pageRecords = applyPage(filteredList, listInDTO.getPageNum(), listInDTO.getPageSize());
+        } else {
+            total = rs.getTotal();
+            pageRecords = outDTOList;
+        }
+        long pageSize = listInDTO.getPageSize() != null ? listInDTO.getPageSize()
+                : (total > 0 ? total : 1);
+        Page<EquipmentBaseOutDTO> outPage = new Page<>(listInDTO.getPageNum() != null ? listInDTO.getPageNum() : 1, pageSize, total);
+        outPage.setRecords(pageRecords);
+        return outPage;
+    }
+
+    /**
+     * 报警级别判定(与前端 buildAlertInfo 保持一致):
+     * 严重报警(1):设备报警状态=1 或 水位/水浸报警;
+     * 一般报警(2):非严重报警且 低电量(<=20%)或 弱信号(<=20%);
+     * 有报警(0):严重报警或一般报警的并集;不传则不过滤。
+     */
+    private boolean matchAlarmLevel(EquipmentBaseOutDTO dto, Integer alarmLevel) {
+        if (alarmLevel == null) {
+            return true;
+        }
+        boolean isCritical = (dto.getEquipmentStatus() != null
+                && Integer.valueOf(1).equals(dto.getEquipmentStatus().getAlarmStatus()))
+                || (dto.getManholeData() != null
+                && ("1".equals(dto.getManholeData().getWaterLevelAlarmStatus())
+                || "1".equals(dto.getManholeData().getWaterInfiltrationAlarmStatus())));
+        if (Integer.valueOf(1).equals(alarmLevel)) {
+            return isCritical;
+        }
+        if (dto.getManholeData() == null) {
+            return Integer.valueOf(0).equals(alarmLevel) && isCritical;
+        }
+        boolean lowBattery = dto.getManholeData().getBatteryLevel() != null
+                && toInt(dto.getManholeData().getBatteryLevel()) <= 20;
+        boolean weakSignal = dto.getManholeData().getSignalStrength() != null
+                && toInt(dto.getManholeData().getSignalStrength()) <= 20;
+        boolean hasAlarm = isCritical || lowBattery || weakSignal;
+        if (Integer.valueOf(0).equals(alarmLevel)) {
+            return hasAlarm;
+        }
+        // alarmLevel = 2:一般报警
+        return hasAlarm && !isCritical;
+    }
+
+    private int toInt(String value) {
+        try {
+            return (int) Double.parseDouble(value.trim());
+        } catch (Exception e) {
+            return Integer.MAX_VALUE;
+        }
+    }
+
+    /**
+     * 内存分页(报警级别过滤后使用)
+     */
+    private List<EquipmentBaseOutDTO> applyPage(List<EquipmentBaseOutDTO> list, Long pageNum, Long pageSize) {
+        if (pageNum == null || pageSize == null) {
+            return list;
+        }
+        int start = (int) ((pageNum - 1) * pageSize);
+        if (start >= list.size()) {
             return new ArrayList<>();
         }
-        List<EquipmentBaseOutDTO> outDTOList = BeanUtil.copyToList(equipmentBaseList, EquipmentBaseOutDTO.class);
-        //查询设备类型
-        List<String> equipmentTypeIdList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentTypeId).distinct().collect(Collectors.toList());
-        LambdaQueryWrapper<EquipmentType> batchQw = new LambdaQueryWrapper<>();
-        batchQw.in(EquipmentType::getId, equipmentTypeIdList);
-        List<EquipmentType> equipmentTypeList = equipmentTypeMapper.selectList(batchQw);
+        int end = (int) Math.min(start + pageSize, list.size());
+        return new ArrayList<>(list.subList(start, end));
+    }
+
+    /**
+     * 窨井盖实时监测-设备报警监控列表
+     * 查询所有窨井类型设备(equipment_type_id IN (9,10))的 jg_device_data 最新一条记录
+     * 告警等级判定:
+     *   严重告警(critical) — 倾斜角度超阈值 / 水浸报警 / 水位报警
+     *   一般告警(warning)  — 低电量(battery<=20) / 弱信号(signal<=20)
+     */
+    @Override
+    public AlarmMonitorListOutDTO getAlarmMonitorList(AlarmMonitorListInDTO inDTO) {
+        log.info("窨井盖实时监测-设备报警监控列表入参:{}", inDTO);
+        // 仅查询窨井类型设备:排水窨井(9)/供水窨井(10)
+        List<String> manholeTypeIds = Arrays.asList("5", "9", "10");
+        LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                .and(StringUtils.isNotBlank(inDTO.getKeyword()), w -> w
+                        .like(EquipmentBase::getEquipmentCode, inDTO.getKeyword())
+                        .or()
+                        .like(EquipmentBase::getEquipmentName, inDTO.getKeyword())
+                        .or()
+                        .like(EquipmentBase::getEquipmentLocation, inDTO.getKeyword()))
+                .orderByDesc(EquipmentBase::getUpdateTime);
+        List<EquipmentBase> equipmentList = this.list(queryWrapper);
+        if (CollUtil.isEmpty(equipmentList)) {
+            AlarmMonitorListOutDTO emptyResult = new AlarmMonitorListOutDTO();
+            emptyResult.setDevices(new ArrayList<>());
+            Map<String, Object> emptyStats = new HashMap<>();
+            emptyStats.put("total", 0);
+            emptyStats.put("critical", 0);
+            emptyStats.put("warning", 0);
+            emptyStats.put("normal", 0);
+            emptyStats.put("onlineCount", 0);
+            emptyStats.put("avgBattery", 0);
+            emptyResult.setStatistics(emptyStats);
+            return emptyResult;
+        }
+        List<EquipmentBaseOutDTO> outDTOList = BeanUtil.copyToList(equipmentList, EquipmentBaseOutDTO.class);
+
+        // 批量查询设备类型名称
+        List<String> typeIdList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentTypeId).distinct().collect(Collectors.toList());
+        List<EquipmentType> equipmentTypeList = equipmentTypeMapper.selectList(new LambdaQueryWrapper<EquipmentType>()
+                .in(EquipmentType::getId, typeIdList));
         Map<String, EquipmentType> equipmentTypeMap = new HashMap<>();
-        if(CollUtil.isNotEmpty(equipmentTypeList)){
-            equipmentTypeMap.putAll(equipmentTypeList.stream().collect(
-                Collectors.toMap(t -> String.valueOf(t.getId()), Function.identity(), (key1, key2) -> key2)));
+        if (CollUtil.isNotEmpty(equipmentTypeList)) {
+            equipmentTypeMap.putAll(equipmentTypeList.stream().collect(Collectors.toMap(t -> String.valueOf(t.getId()), Function.identity(), (k1, k2) -> k2)));
         }
-        //查询设备状态
+
+        // 批量查询设备运维状态
         List<String> equipmentIdList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentId).distinct().collect(Collectors.toList());
         List<EquipmentStatus> equipmentStatusList = equipmentStatusMapper.selectList(new LambdaQueryWrapper<EquipmentStatus>()
                 .in(EquipmentStatus::getEquipmentId, equipmentIdList));
-        Map<String, EquipmentStatus> EquipmentStatusMap = new HashMap<>();
-        if(CollUtil.isNotEmpty(equipmentStatusList)){
-            EquipmentStatusMap.putAll(equipmentStatusList.stream().collect(Collectors.toMap(EquipmentStatus::getEquipmentId, Function.identity(), (key1, key2) -> key2)));
-        }
-        //查询设备监测状态
-        Map<String, List<ManholeData>> imeiCardNumberMapList = new HashMap<>();
-        if(listInDTO.getIsQueryManholeData()){
-            List<String> equipmentCodeList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentCode).distinct().collect(Collectors.toList());
-            List<ManholeData> manholeDataList = manholeDataMapper.selectList(new LambdaQueryWrapper<ManholeData>()
-                    .in(ManholeData::getImeiCardNumber, equipmentCodeList)
-                    .orderByDesc(ManholeData::getCreateTime));
-            imeiCardNumberMapList.putAll(manholeDataList.stream().collect(Collectors.groupingBy(ManholeData::getImeiCardNumber)));
-        }
-        List<ManholeData> manholeDataList = null;
-        for(EquipmentBaseOutDTO outDTO : outDTOList) {
+        Map<String, EquipmentStatus> equipmentStatusMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(equipmentStatusList)) {
+            equipmentStatusMap.putAll(equipmentStatusList.stream().collect(Collectors.toMap(EquipmentStatus::getEquipmentId, Function.identity(), (k1, k2) -> k2)));
+        }
+
+        // 批量查询最新 jg_device_data 记录(按 imei_card_number = equipment_code 关联)
+        List<String> equipmentCodeList = outDTOList.stream().map(EquipmentBaseOutDTO::getEquipmentCode).distinct().collect(Collectors.toList());
+        List<ManholeData> manholeDataList = manholeDataMapper.selectList(new LambdaQueryWrapper<ManholeData>()
+                .in(ManholeData::getImeiCardNumber, equipmentCodeList)
+                .orderByDesc(ManholeData::getCreateTime));
+        Map<String, List<ManholeData>> imeiMapList = new HashMap<>();
+        if (CollUtil.isNotEmpty(manholeDataList)) {
+            imeiMapList.putAll(manholeDataList.stream().collect(Collectors.groupingBy(ManholeData::getImeiCardNumber)));
+        }
+
+        // 批量查询未办结工单(待派单/待接单/已接单/处理中/待验收/延期审核)
+        List<Integer> activeStatusList = Arrays.asList(1, 2, 3, 4, 5, 8);
+        List<WorkOrder> activeWorkOrders = workOrderMapper.selectList(new LambdaQueryWrapper<WorkOrder>()
+                .in(WorkOrder::getDeviceId, equipmentIdList)
+                .in(WorkOrder::getOrderStatus, activeStatusList));
+        Set<String> deviceWithActiveOrder = activeWorkOrders.stream()
+                .map(WorkOrder::getDeviceId)
+                .collect(Collectors.toSet());
+
+        // 统计变量
+        int critical = 0, warning = 0, normal = 0, onlineCount = 0;
+        double batterySum = 0;
+        int batteryCount = 0;
+
+        for (EquipmentBaseOutDTO outDTO : outDTOList) {
+            // 设备类型名称
             EquipmentType equipmentType = equipmentTypeMap.get(outDTO.getEquipmentTypeId());
-            if(equipmentType != null){
+            if (equipmentType != null) {
                 outDTO.setEquipmentTypeName(equipmentType.getTypeName());
             }
-            EquipmentStatus equipmentStatus = EquipmentStatusMap.get(outDTO.getEquipmentId());
-            if(equipmentStatus != null){
-                EquipmentStatusOutDTO equipmentStatusOutDTO = BeanUtil.copyProperties(equipmentStatus, EquipmentStatusOutDTO.class);
-                outDTO.setEquipmentStatus(equipmentStatusOutDTO);
+            // 设备运维状态
+            EquipmentStatus equipmentStatus = equipmentStatusMap.get(outDTO.getEquipmentId());
+            if (equipmentStatus != null) {
+                EquipmentStatusOutDTO statusOutDTO = BeanUtil.copyProperties(equipmentStatus, EquipmentStatusOutDTO.class);
+                outDTO.setEquipmentStatus(statusOutDTO);
+                if (Integer.valueOf(1).equals(equipmentStatus.getOnlineStatus())) {
+                    onlineCount++;
+                }
             }
-            if(listInDTO.getIsQueryManholeData()){
-                manholeDataList = imeiCardNumberMapList.get(outDTO.getEquipmentCode());
-                if(CollUtil.isNotEmpty(manholeDataList)){
-                    ManholeData manholeData = manholeDataList.get(0);
-                    ManholeDataOutDTO manholeDataOutDTO = BeanUtil.copyProperties(manholeData, ManholeDataOutDTO.class);
-                    outDTO.setManholeData(manholeDataOutDTO);
+            // 是否有未办结工单
+            outDTO.setWorkOrderSubmitted(deviceWithActiveOrder.contains(outDTO.getEquipmentId()));
+            // 最新 jg_device_data 记录
+            List<ManholeData> dataList = imeiMapList.get(outDTO.getEquipmentCode());
+            if (CollUtil.isNotEmpty(dataList)) {
+                ManholeData latest = dataList.get(0);
+                ManholeDataOutDTO manholeDataOutDTO = BeanUtil.copyProperties(latest, ManholeDataOutDTO.class);
+                outDTO.setManholeData(manholeDataOutDTO);
+
+                // 统计电量
+                if (StringUtils.isNotBlank(latest.getBatteryLevel())) {
+                    try {
+                        batterySum += Double.parseDouble(latest.getBatteryLevel().trim());
+                        batteryCount++;
+                    } catch (NumberFormatException ignored) {}
                 }
+
+                // 告警等级判定
+                boolean isCritical = isCriticalAlarm(latest);
+                boolean isWarning = !isCritical && isGeneralAlarm(latest);
+                if (isCritical) critical++;
+                else if (isWarning) warning++;
+                else normal++;
+            } else {
+                normal++;
             }
         }
-        return outDTOList;
+
+        // 构建统计信息
+        Map<String, Object> statistics = new HashMap<>();
+        statistics.put("total", outDTOList.size());
+        statistics.put("critical", critical);
+        statistics.put("warning", warning);
+        statistics.put("normal", normal);
+        statistics.put("onlineCount", onlineCount);
+        statistics.put("avgBattery", batteryCount > 0 ? (int) (batterySum / batteryCount) : 0);
+
+        AlarmMonitorListOutDTO result = new AlarmMonitorListOutDTO();
+        result.setDevices(outDTOList);
+        result.setStatistics(statistics);
+        return result;
+    }
+
+    /**
+     * 严重告警判定:倾斜角度超阈值 / 水浸报警 / 水位报警
+     */
+    private boolean isCriticalAlarm(ManholeData data) {
+        if (data == null) return false;
+        // 水浸报警
+        if ("1".equals(data.getWaterInfiltrationAlarmStatus())) return true;
+        // 水位报警
+        if ("1".equals(data.getWaterLevelAlarmStatus())) return true;
+        // 倾斜角度超阈值
+        if (StringUtils.isNotBlank(data.getTiltAngle()) && StringUtils.isNotBlank(data.getAngleAlarmThreshold())) {
+            try {
+                double angle = Double.parseDouble(data.getTiltAngle().trim());
+                double threshold = Double.parseDouble(data.getAngleAlarmThreshold().trim());
+                if (angle > threshold) return true;
+            } catch (NumberFormatException ignored) {}
+        }
+        return false;
+    }
+
+    /**
+     * 一般告警判定:低电量(battery<=20) / 弱信号(signal<=20)
+     */
+    private boolean isGeneralAlarm(ManholeData data) {
+        if (data == null) return false;
+        if (StringUtils.isNotBlank(data.getBatteryLevel())) {
+            try {
+                if (Double.parseDouble(data.getBatteryLevel().trim()) <= 20) return true;
+            } catch (NumberFormatException ignored) {}
+        }
+        if (StringUtils.isNotBlank(data.getSignalStrength())) {
+            try {
+                if (Double.parseDouble(data.getSignalStrength().trim()) <= 20) return true;
+            } catch (NumberFormatException ignored) {}
+        }
+        return false;
     }
 
     @Override

+ 429 - 4
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/MaintenanceDataStatServiceImpl.java

@@ -4,16 +4,33 @@ import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentStatus;
+import com.zksy.base.domain.EquipmentType;
 import com.zksy.base.domain.MaintenanceDataStat;
+import com.zksy.base.domain.WorkOrder;
+import com.zksy.base.manhole.domain.ManholeData;
+import com.zksy.base.manhole.mapper.ManholeDataMapper;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.EquipmentStatusMapper;
+import com.zksy.base.mapper.EquipmentTypeMapper;
 import com.zksy.base.mapper.MaintenanceDataStatMapper;
+import com.zksy.base.mapper.WorkOrderMapper;
 import com.zksy.base.service.MaintenanceDataStatService;
 import com.zksy.common.exception.ServiceException;
+import com.zksy.manhole.dto.in.MaintenanceDataStatDashboardInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatQueryInDTO;
 import com.zksy.manhole.dto.in.MaintenanceDataStatTrendInDTO;
 import com.zksy.manhole.dto.out.AbnormalTerminalTrendOutDTO;
 import com.zksy.manhole.dto.out.CaseRateTrendOutDTO;
+import com.zksy.manhole.dto.out.EquipmentBaseOutDTO;
+import com.zksy.manhole.dto.out.EquipmentStatusOutDTO;
+import com.zksy.manhole.dto.out.MaintenanceDataStatDashboardOutDTO;
 import com.zksy.manhole.dto.out.MaintenanceDataStatOutDTO;
+import com.zksy.manhole.dto.out.ManholeDataOutDTO;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
@@ -23,6 +40,8 @@ import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.WeekFields;
 import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
 * @author Administrator
@@ -34,6 +53,21 @@ import java.util.*;
 public class MaintenanceDataStatServiceImpl extends ServiceImpl<MaintenanceDataStatMapper, MaintenanceDataStat>
     implements MaintenanceDataStatService{
 
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+
+    @Autowired
+    private EquipmentTypeMapper equipmentTypeMapper;
+
+    @Autowired
+    private EquipmentStatusMapper equipmentStatusMapper;
+
+    @Autowired
+    private ManholeDataMapper manholeDataMapper;
+
+    @Autowired
+    private WorkOrderMapper workOrderMapper;
+
     /**
      * 根据日期查询运维数据统计
      * @param queryInDTO
@@ -128,6 +162,401 @@ public class MaintenanceDataStatServiceImpl extends ServiceImpl<MaintenanceDataS
         return result;
     }
 
+    /**
+     * 运维看板统计数据:一次返回KPI指标、异常终端趋势、处置率/延期率趋势、问题类型分布、区域排行、异常终端列表
+     * 数据来源:
+     *   - KPI/问题类型分布/区域排行/异常终端列表:equipment_base + equipment_status + jg_device_data + work_order 实时计算
+     *   - 趋势:日期范围内 jg_device_data 历史记录与 work_order 按周度/月度分组统计
+     * @param inDTO
+     * @return
+     */
+    @Override
+    public MaintenanceDataStatDashboardOutDTO getDashboardStat(MaintenanceDataStatDashboardInDTO inDTO) {
+        log.info("运维看板统计数据-入参:{}", inDTO);
+        MaintenanceDataStatDashboardOutDTO outDTO = new MaintenanceDataStatDashboardOutDTO();
+
+        // ============ 日期范围:默认近6个月 ============
+        Date endDate = inDTO.getEndDate() != null ? inDTO.getEndDate() : new Date();
+        Date startDate = inDTO.getStartDate();
+        if (startDate == null) {
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(endDate);
+            cal.add(Calendar.MONTH, -5);
+            cal.set(Calendar.DAY_OF_MONTH, 1);
+            startDate = cal.getTime();
+        }
+        int trendType = inDTO.getTrendType() != null ? inDTO.getTrendType() : 2;
+
+        // ============ 1. 查询窨井设备(排水9/供水10/燃气5) ============
+        List<String> manholeTypeIds = Arrays.asList("5", "9", "10");
+        List<EquipmentBase> equipmentList = equipmentBaseMapper.selectList(
+                new LambdaQueryWrapper<EquipmentBase>()
+                        .in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                        .orderByDesc(EquipmentBase::getUpdateTime));
+        if (CollUtil.isEmpty(equipmentList)) {
+            outDTO.setKpi(buildEmptyKpi());
+            return outDTO;
+        }
+        List<EquipmentBaseOutDTO> deviceList = BeanUtil.copyToList(equipmentList, EquipmentBaseOutDTO.class);
+        List<String> equipmentIdList = deviceList.stream().map(EquipmentBaseOutDTO::getEquipmentId).distinct().collect(Collectors.toList());
+        List<String> equipmentCodeList = deviceList.stream().map(EquipmentBaseOutDTO::getEquipmentCode).distinct().collect(Collectors.toList());
+
+        // ============ 2. 批量查询设备类型名称 ============
+        Map<String, String> typeNameMap = new HashMap<>();
+        List<EquipmentType> equipmentTypeList = equipmentTypeMapper.selectList(
+                new LambdaQueryWrapper<EquipmentType>().in(EquipmentType::getId, equipmentTypeIdsOf(deviceList)));
+        if (CollUtil.isNotEmpty(equipmentTypeList)) {
+            typeNameMap = equipmentTypeList.stream()
+                    .collect(Collectors.toMap(t -> String.valueOf(t.getId()), EquipmentType::getTypeName, (a, b) -> b));
+        }
+
+        // ============ 3. 批量查询设备运维状态 ============
+        Map<String, EquipmentStatus> equipmentStatusMap = new HashMap<>();
+        List<EquipmentStatus> equipmentStatusList = equipmentStatusMapper.selectList(
+                new LambdaQueryWrapper<EquipmentStatus>().in(EquipmentStatus::getEquipmentId, equipmentIdList));
+        if (CollUtil.isNotEmpty(equipmentStatusList)) {
+            equipmentStatusMap = equipmentStatusList.stream()
+                    .collect(Collectors.toMap(EquipmentStatus::getEquipmentId, Function.identity(), (a, b) -> a));
+        }
+
+        // ============ 4. 批量查询最新 jg_device_data 记录 ============
+        Map<String, List<ManholeData>> imeiDataMap = new HashMap<>();
+        List<ManholeData> manholeDataList = manholeDataMapper.selectList(
+                new LambdaQueryWrapper<ManholeData>()
+                        .in(ManholeData::getImeiCardNumber, equipmentCodeList)
+                        .orderByDesc(ManholeData::getCreateTime));
+        if (CollUtil.isNotEmpty(manholeDataList)) {
+            imeiDataMap = manholeDataList.stream().collect(Collectors.groupingBy(ManholeData::getImeiCardNumber));
+        }
+
+        // ============ 5. 批量查询未办结工单(待派单/待接单/已接单/处理中/待验收/延期审核) ============
+        List<Integer> activeStatusList = Arrays.asList(1, 2, 3, 4, 5, 8);
+        List<WorkOrder> activeWorkOrders = workOrderMapper.selectList(
+                new LambdaQueryWrapper<WorkOrder>()
+                        .in(WorkOrder::getDeviceId, equipmentIdList)
+                        .in(WorkOrder::getOrderStatus, activeStatusList));
+        Set<String> deviceWithActiveOrder = CollUtil.isNotEmpty(activeWorkOrders)
+                ? activeWorkOrders.stream().map(WorkOrder::getDeviceId).collect(Collectors.toSet())
+                : new HashSet<>();
+
+        // ============ 6. 逐设备判定异常并组装 ============
+        List<EquipmentBaseOutDTO> abnormalDeviceList = new ArrayList<>();
+        int abnormalCount = 0, submittedCount = 0;
+        Map<String, Integer> problemTypeMap = new LinkedHashMap<>();
+        Map<String, int[]> districtStatMap = new LinkedHashMap<>();
+
+        for (EquipmentBaseOutDTO out : deviceList) {
+            // 设备类型名称
+            String typeName = typeNameMap.get(out.getEquipmentTypeId());
+            out.setEquipmentTypeName(typeName);
+
+            // 设备运维状态
+            EquipmentStatus equipmentStatus = equipmentStatusMap.get(out.getEquipmentId());
+            if (equipmentStatus != null) {
+                out.setEquipmentStatus(BeanUtil.copyProperties(equipmentStatus, EquipmentStatusOutDTO.class));
+            }
+            // 是否有未办结工单
+            out.setWorkOrderSubmitted(deviceWithActiveOrder.contains(out.getEquipmentId()));
+
+            // 最新监测数据
+            ManholeData latest = null;
+            List<ManholeData> dataList = imeiDataMap.get(out.getEquipmentCode());
+            if (CollUtil.isNotEmpty(dataList)) {
+                latest = dataList.get(0);
+                out.setManholeData(BeanUtil.copyProperties(latest, ManholeDataOutDTO.class));
+            }
+
+            // 异常判定
+            List<String> faultTypes = judgeFaultTypes(latest, equipmentStatus);
+            boolean isAbnormal = CollUtil.isNotEmpty(faultTypes);
+            if (isAbnormal) {
+                abnormalCount++;
+                if (Boolean.TRUE.equals(out.getWorkOrderSubmitted())) {
+                    submittedCount++;
+                }
+                // 问题类型分布
+                for (String faultType : faultTypes) {
+                    problemTypeMap.merge(faultType, 1, Integer::sum);
+                }
+                abnormalDeviceList.add(out);
+            }
+            // 区域排行
+            String district = StringUtils.isNotBlank(out.getDistrict()) ? out.getDistrict() : "未分配";
+            int[] stat = districtStatMap.computeIfAbsent(district, k -> new int[2]);
+            stat[0]++;
+            if (isAbnormal) {
+                stat[1]++;
+            }
+        }
+
+        // ============ 7. KPI 指标 ============
+        Map<String, Object> kpi = new LinkedHashMap<>();
+        kpi.put("totalTerminal", equipmentList.size());
+        kpi.put("abnormalTerminal", abnormalCount);
+        kpi.put("abnormalRate", equipmentList.size() > 0 ? round1(abnormalCount * 100.0 / equipmentList.size()) : 0);
+        kpi.put("submittedCount", submittedCount);
+        kpi.put("solvedRate", abnormalCount > 0 ? round1(submittedCount * 100.0 / abnormalCount) : 0);
+        // 工单统计(日期范围内)
+        List<WorkOrder> rangeOrders = workOrderMapper.selectList(
+                new LambdaQueryWrapper<WorkOrder>()
+                        .ge(WorkOrder::getCreateTime, startDate)
+                        .le(WorkOrder::getCreateTime, endDate));
+        int totalCase = rangeOrders.size();
+        long finishedCase = rangeOrders.stream().filter(o -> Integer.valueOf(6).equals(o.getOrderStatus())).count();
+        long delayCase = rangeOrders.stream().filter(o -> Integer.valueOf(8).equals(o.getOrderStatus()) || Integer.valueOf(9).equals(o.getOrderStatus())).count();
+        kpi.put("totalCase", totalCase);
+        kpi.put("disposalRate", totalCase > 0 ? round1(finishedCase * 100.0 / totalCase) : 0);
+        kpi.put("delayRate", totalCase > 0 ? round1(delayCase * 100.0 / totalCase) : 0);
+        // 平均处置时长(已结案工单:实际完成时间-派单时间,小时)
+        kpi.put("avgDisposalTime", calcAvgDisposalTime(rangeOrders));
+        outDTO.setKpi(kpi);
+
+        // ============ 8. 异常终端趋势(基于 jg_device_data 历史记录) ============
+        outDTO.setAbnormalTerminalTrend(buildAbnormalTerminalTrend(equipmentCodeList, startDate, endDate, trendType));
+
+        // ============ 9. 处置率/延期率趋势(基于 work_order) ============
+        outDTO.setCaseRateTrend(buildCaseRateTrend(rangeOrders, startDate, endDate, trendType));
+
+        // ============ 10. 问题类型分布 ============
+        List<Map<String, Object>> problemTypeDistribution = new ArrayList<>();
+        for (Map.Entry<String, Integer> entry : problemTypeMap.entrySet()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("name", entry.getKey());
+            item.put("value", entry.getValue());
+            problemTypeDistribution.add(item);
+        }
+        outDTO.setProblemTypeDistribution(problemTypeDistribution);
+
+        // ============ 11. 区域排行 ============
+        List<Map<String, Object>> regionRank = new ArrayList<>();
+        for (Map.Entry<String, int[]> entry : districtStatMap.entrySet()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("name", entry.getKey());
+            item.put("total", entry.getValue()[0]);
+            item.put("abnormal", entry.getValue()[1]);
+            regionRank.add(item);
+        }
+        regionRank.sort((a, b) -> ((Integer) b.get("abnormal")) - ((Integer) a.get("abnormal")));
+        outDTO.setRegionRank(regionRank);
+
+        // ============ 12. 异常终端列表 ============
+        outDTO.setAbnormalDevices(abnormalDeviceList);
+        return outDTO;
+    }
+
+    /**
+     * 异常判定:倾斜超标/水位超限/水浸报警/通讯故障/电池低电量(<=15)/信号弱(<=20)
+     * 注意:equipmentStatus 为 null 时不判定通讯故障(用于历史记录趋势统计,无法获知当时在线状态)
+     */
+    private List<String> judgeFaultTypes(ManholeData data, EquipmentStatus equipmentStatus) {
+        List<String> faultTypes = new ArrayList<>();
+        boolean hasStatus = equipmentStatus != null;
+        boolean online = hasStatus && Integer.valueOf(1).equals(equipmentStatus.getOnlineStatus());
+        if (data == null) {
+            // 无监测数据:有状态记录且不在线视为通讯故障,无状态记录也视为通讯故障
+            if (!hasStatus || !online) {
+                faultTypes.add("通讯故障");
+            }
+            return faultTypes;
+        }
+        if (hasStatus && !online) {
+            faultTypes.add("通讯故障");
+        }
+        // 倾斜超标
+        if (isOverThreshold(data.getTiltAngle(), data.getAngleAlarmThreshold())) {
+            faultTypes.add("倾斜超标");
+        }
+        // 水位超限
+        if ("1".equals(data.getWaterLevelAlarmStatus())) {
+            faultTypes.add("水位超限");
+        }
+        // 水浸报警
+        if ("1".equals(data.getWaterInfiltrationAlarmStatus())) {
+            faultTypes.add("水浸报警");
+        }
+        // 电池低电量(<=15%)
+        if (isNumberNotAbove(data.getBatteryLevel(), 15)) {
+            faultTypes.add("电池低电量");
+        }
+        // 信号弱(<=20)
+        if (isNumberNotAbove(data.getSignalStrength(), 20)) {
+            faultTypes.add("信号弱");
+        }
+        return faultTypes;
+    }
+
+    /**
+     * 数值字段超过阈值判定
+     */
+    private boolean isOverThreshold(String value, String threshold) {
+        if (StringUtils.isBlank(value) || StringUtils.isBlank(threshold)) {
+            return false;
+        }
+        try {
+            return Double.parseDouble(value.trim()) > Double.parseDouble(threshold.trim());
+        } catch (NumberFormatException ignored) {
+            return false;
+        }
+    }
+
+    /**
+     * 数值字段不大于阈值判定
+     */
+    private boolean isNumberNotAbove(String value, double maxValue) {
+        if (StringUtils.isBlank(value)) {
+            return false;
+        }
+        try {
+            double v = Double.parseDouble(value.trim());
+            return v > 0 && v <= maxValue;
+        } catch (NumberFormatException ignored) {
+            return false;
+        }
+    }
+
+    /**
+     * 平均处置时长:已结案工单 (realFinishTime - dispatchTime) 平均小时
+     */
+    private double calcAvgDisposalTime(List<WorkOrder> orders) {
+        if (CollUtil.isEmpty(orders)) {
+            return 0;
+        }
+        List<WorkOrder> finishedOrders = orders.stream()
+                .filter(o -> Integer.valueOf(6).equals(o.getOrderStatus()))
+                .filter(o -> o.getRealFinishTime() != null && o.getDispatchTime() != null)
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(finishedOrders)) {
+            return 0;
+        }
+        double totalHours = 0;
+        for (WorkOrder order : finishedOrders) {
+            long diffMillis = order.getRealFinishTime().getTime() - order.getDispatchTime().getTime();
+            totalHours += diffMillis / (1000.0 * 60 * 60);
+        }
+        return round1(totalHours / finishedOrders.size());
+    }
+
+    /**
+     * 异常终端趋势:按周度/月度分组统计 jg_device_data 历史记录中的异常设备去重数
+     */
+    private List<Map<String, Object>> buildAbnormalTerminalTrend(List<String> equipmentCodeList,
+                                                                 Date startDate, Date endDate, int trendType) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (CollUtil.isEmpty(equipmentCodeList)) {
+            return result;
+        }
+        List<ManholeData> historyDataList = manholeDataMapper.selectList(
+                new LambdaQueryWrapper<ManholeData>()
+                        .in(ManholeData::getImeiCardNumber, equipmentCodeList)
+                        .ge(ManholeData::getCreateTime, startDate)
+                        .le(ManholeData::getCreateTime, endDate)
+                        .orderByAsc(ManholeData::getCreateTime));
+        if (CollUtil.isEmpty(historyDataList)) {
+            return result;
+        }
+        Map<String, Set<String>> periodAbnormalDevices = new LinkedHashMap<>();
+        Map<String, Set<String>> periodAllDevices = new LinkedHashMap<>();
+        for (ManholeData data : historyDataList) {
+            String periodKey = formatPeriodKey(data.getCreateTime(), trendType);
+            if (periodKey == null) {
+                continue;
+            }
+            periodAllDevices.computeIfAbsent(periodKey, k -> new HashSet<>()).add(data.getImeiCardNumber());
+            if (CollUtil.isNotEmpty(judgeFaultTypes(data, null))) {
+                periodAbnormalDevices.computeIfAbsent(periodKey, k -> new HashSet<>()).add(data.getImeiCardNumber());
+            }
+        }
+        for (Map.Entry<String, Set<String>> entry : periodAbnormalDevices.entrySet()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("trendLabel", entry.getKey());
+            item.put("abnormalTerminal", entry.getValue().size());
+            item.put("totalTerminal", periodAllDevices.getOrDefault(entry.getKey(), new HashSet<>()).size());
+            result.add(item);
+        }
+        result.sort(Comparator.comparing(m -> String.valueOf(m.get("trendLabel"))));
+        return result;
+    }
+
+    /**
+     * 处置率/延期率趋势:按周度/月度分组统计工单
+     */
+    private List<Map<String, Object>> buildCaseRateTrend(List<WorkOrder> orders,
+                                                         Date startDate, Date endDate, int trendType) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (CollUtil.isEmpty(orders)) {
+            return result;
+        }
+        Map<String, List<WorkOrder>> periodGroup = new LinkedHashMap<>();
+        for (WorkOrder order : orders) {
+            String periodKey = formatPeriodKey(order.getCreateTime(), trendType);
+            if (periodKey != null) {
+                periodGroup.computeIfAbsent(periodKey, k -> new ArrayList<>()).add(order);
+            }
+        }
+        for (Map.Entry<String, List<WorkOrder>> entry : periodGroup.entrySet()) {
+            List<WorkOrder> periodOrders = entry.getValue();
+            long finished = periodOrders.stream().filter(o -> Integer.valueOf(6).equals(o.getOrderStatus())).count();
+            long delay = periodOrders.stream().filter(o -> Integer.valueOf(8).equals(o.getOrderStatus()) || Integer.valueOf(9).equals(o.getOrderStatus())).count();
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("trendLabel", entry.getKey());
+            item.put("caseDisposalRate", periodOrders.size() > 0 ? round1(finished * 100.0 / periodOrders.size()) : 0);
+            item.put("caseDelayRate", periodOrders.size() > 0 ? round1(delay * 100.0 / periodOrders.size()) : 0);
+            result.add(item);
+        }
+        result.sort(Comparator.comparing(m -> String.valueOf(m.get("trendLabel"))));
+        return result;
+    }
+
+    /**
+     * 周期标签:周度 yyyy-Www / 月度 yyyy-MM
+     */
+    private String formatPeriodKey(Date date, int trendType) {
+        if (date == null) {
+            return null;
+        }
+        LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+        if (trendType == 1) {
+            WeekFields weekFields = WeekFields.of(Locale.getDefault());
+            int weekYear = localDate.get(weekFields.weekBasedYear());
+            int weekNum = localDate.get(weekFields.weekOfWeekBasedYear());
+            return String.format("%d-W%02d", weekYear, weekNum);
+        }
+        return localDate.format(DateTimeFormatter.ofPattern("yyyy-MM"));
+    }
+
+    /**
+     * 设备类型ID列表(避免空集合导致SQL错误)
+     */
+    private List<String> equipmentTypeIdsOf(List<EquipmentBaseOutDTO> deviceList) {
+        List<String> typeIds = deviceList.stream().map(EquipmentBaseOutDTO::getEquipmentTypeId)
+                .filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());
+        return typeIds.isEmpty() ? Collections.singletonList("-1") : typeIds;
+    }
+
+    /**
+     * 保留一位小数
+     */
+    private double round1(double value) {
+        return Math.round(value * 10) / 10.0;
+    }
+
+    /**
+     * 空数据KPI
+     */
+    private Map<String, Object> buildEmptyKpi() {
+        Map<String, Object> kpi = new LinkedHashMap<>();
+        kpi.put("totalTerminal", 0);
+        kpi.put("abnormalTerminal", 0);
+        kpi.put("abnormalRate", 0);
+        kpi.put("submittedCount", 0);
+        kpi.put("solvedRate", 0);
+        kpi.put("totalCase", 0);
+        kpi.put("disposalRate", 0);
+        kpi.put("delayRate", 0);
+        kpi.put("avgDisposalTime", 0);
+        return kpi;
+    }
+
     /**
      * 查询统计数据列表(公共方法)
      * @param inDTO
@@ -172,7 +601,3 @@ public class MaintenanceDataStatServiceImpl extends ServiceImpl<MaintenanceDataS
         return groupedMap;
     }
 }
-
-
-
-

+ 235 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/ManholeLayerServiceImpl.java

@@ -0,0 +1,235 @@
+package com.zksy.base.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.alarm.service.AlarmDataService;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentStatus;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.manhole.domain.ManholeData;
+import com.zksy.base.manhole.service.ManholeDataService;
+import com.zksy.base.service.EquipmentBaseService;
+import com.zksy.base.service.EquipmentStatusService;
+import com.zksy.base.service.EquipmentTypeService;
+import com.zksy.base.service.ManholeLayerService;
+import com.zksy.manhole.dto.in.ManholeDeviceListInDTO;
+import com.zksy.manhole.dto.out.EquipmentBaseOutDTO;
+import com.zksy.manhole.dto.out.ManholeLayerStatisticsOutDTO;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 态势图层 - 窨井设备服务实现
+ */
+@Service
+public class ManholeLayerServiceImpl implements ManholeLayerService {
+
+    @Autowired
+    private EquipmentBaseService equipmentBaseService;
+
+    @Autowired
+    private EquipmentStatusService equipmentStatusService;
+
+    @Autowired
+    private EquipmentTypeService equipmentTypeService;
+
+    @Autowired
+    private ManholeDataService manholeDataService;
+
+    @Autowired
+    private AlarmDataService alarmDataService;
+
+    /**
+     * 获取态势图层数据(设备列表 + 统计)
+     */
+    @Override
+    public Map<String, Object> getManholeLayerData(ManholeDeviceListInDTO listInDTO) {
+        // 1. 仅查窨井类型设备:排水窨井(equipment_type_id=9)/供水窨井(equipment_type_id=10)
+        List<String> manholeTypeIds = Arrays.asList("5", "9", "10");
+
+        // 2. 构建设备基础信息查询条件(不按状态预过滤,前端做前端过滤)
+        LambdaQueryWrapper<EquipmentBase> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(EquipmentBase::getEquipmentTypeId, manholeTypeIds)
+                .and(StrUtil.isNotBlank(listInDTO.getKeyword()), wrapper -> wrapper
+                        .like(EquipmentBase::getEquipmentCode, listInDTO.getKeyword())
+                        .or()
+                        .like(EquipmentBase::getEquipmentName, listInDTO.getKeyword())
+                        .or()
+                        .like(EquipmentBase::getEquipmentLocation, listInDTO.getKeyword()))
+                .orderByDesc(EquipmentBase::getUpdateTime);
+
+        List<EquipmentBase> deviceList = equipmentBaseService.list(queryWrapper);
+        if (CollUtil.isEmpty(deviceList)) {
+            Map<String, Object> result = new HashMap<>();
+            result.put("devices", Collections.emptyList());
+            ManholeLayerStatisticsOutDTO stats = new ManholeLayerStatisticsOutDTO();
+            stats.setTotal(0);
+            result.put("statistics", stats);
+            return result;
+        }
+
+        // 4. 查询所有设备的状态信息(LEFT JOIN 模式:即使设备状态表无记录也要保留设备)
+        List<String> equipmentIdList = deviceList.stream()
+                .map(EquipmentBase::getEquipmentId)
+                .distinct()
+                .collect(Collectors.toList());
+        Map<String, EquipmentStatus> statusMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(equipmentIdList)) {
+            // 按 equipment_id 查询状态(非主键 status_id),确保 LEFT JOIN 语义
+            List<EquipmentStatus> allStatusList = equipmentStatusService.list(
+                    new LambdaQueryWrapper<EquipmentStatus>()
+                            .in(EquipmentStatus::getEquipmentId, equipmentIdList));
+            if (CollUtil.isNotEmpty(allStatusList)) {
+                statusMap.putAll(allStatusList.stream().collect(
+                        Collectors.toMap(EquipmentStatus::getEquipmentId, Function.identity(), (key1, key2) -> key2)));
+            }
+        }
+
+        // 5. 查询设备类型名称
+        List<String> equipmentTypeIdList = deviceList.stream()
+                .map(EquipmentBase::getEquipmentTypeId)
+                .distinct()
+                .collect(Collectors.toList());
+        Map<String, EquipmentType> typeMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(equipmentTypeIdList)) {
+            List<EquipmentType> typeList = equipmentTypeService.list(new LambdaQueryWrapper<EquipmentType>()
+                    .in(EquipmentType::getId, equipmentTypeIdList));
+            if (CollUtil.isNotEmpty(typeList)) {
+                typeMap = typeList.stream().collect(
+                        Collectors.toMap(EquipmentType::getId, Function.identity(), (key1, key2) -> key2));
+            }
+        }
+
+        // 6. 查询井盖监测数据(最新一条)
+        Map<String, List<ManholeData>> imeiCardNumberMapList = new HashMap<>();
+        if (Boolean.TRUE.equals(listInDTO.getIsQueryManholeData())) {
+            List<String> equipmentCodeList = deviceList.stream()
+                    .map(EquipmentBase::getEquipmentCode)
+                    .distinct()
+                    .collect(Collectors.toList());
+            List<ManholeData> manholeDataList = manholeDataService.list(new LambdaQueryWrapper<ManholeData>()
+                    .in(ManholeData::getImeiCardNumber, equipmentCodeList)
+                    .orderByDesc(ManholeData::getCreateTime));
+            imeiCardNumberMapList = manholeDataList.stream()
+                    .collect(Collectors.groupingBy(ManholeData::getImeiCardNumber));
+        }
+
+        // 7. 查询报警数据(最新一条)
+        Map<String, List<AlarmData>> alarmDataMap = new HashMap<>();
+        // 有状态的才查报警数据
+        List<String> statusEquipmentIds = new ArrayList<>(statusMap.keySet());
+        if (CollUtil.isNotEmpty(statusEquipmentIds)) {
+            List<String> equipmentCodes = deviceList.stream()
+                    .filter(d -> statusMap.containsKey(d.getEquipmentId()))
+                    .map(EquipmentBase::getEquipmentCode)
+                    .distinct()
+                    .collect(Collectors.toList());
+            List<AlarmData> alarmDataList = alarmDataService.list(new LambdaQueryWrapper<AlarmData>()
+                    .in(AlarmData::getDeviceCode, equipmentCodes)
+                    .orderByDesc(AlarmData::getAlarmTime));
+            alarmDataMap = alarmDataList.stream()
+                    .collect(Collectors.groupingBy(AlarmData::getDeviceCode));
+        }
+
+        // 8. 组装出参
+        List<EquipmentBaseOutDTO> outDTOList = new ArrayList<>();
+        Map<String, Object> statisticsMap = new HashMap<>();
+        
+        int total = 0;
+        int inUse = 0;
+        int maintenance = 0;
+        int alarm = 0;
+        int normal = 0;
+        int online = 0;
+        int offline = 0;
+
+        for (EquipmentBase device : deviceList) {
+            EquipmentBaseOutDTO outDTO = new EquipmentBaseOutDTO();
+            BeanUtils.copyProperties(device, outDTO);
+
+            // 设备类型名称
+            EquipmentType equipmentType = typeMap.get(device.getEquipmentTypeId());
+            if (equipmentType != null) {
+                outDTO.setEquipmentTypeName(equipmentType.getTypeName());
+            }
+
+            // 设备状态(可能存在但为空的情况)
+            EquipmentStatus status = statusMap.get(device.getEquipmentId());
+            if (status != null) {
+                // 转换 DTO - EquipmentStatusOutDTO 只有 4 个字段
+                com.zksy.manhole.dto.out.EquipmentStatusOutDTO outStatus = new com.zksy.manhole.dto.out.EquipmentStatusOutDTO();
+                outStatus.setCurrentStatus(status.getCurrentStatus());
+                outStatus.setAlarmStatus(status.getAlarmStatus());
+                outStatus.setOnlineStatus(status.getOnlineStatus());
+                // statusUpdateTime 转换为 Date(使用 Hutool)
+                outStatus.setStatusUpdateTime(DateUtil.date(status.getStatusUpdateTime()));
+                outDTO.setEquipmentStatus(outStatus);
+                
+                // 统计:运维状态(精确匹配,不 fallback)
+                if (Objects.equals(status.getCurrentStatus(), 1)) inUse++;
+                else if (Objects.equals(status.getCurrentStatus(), 3)) maintenance++;
+                
+                // 统计:报警状态(alarmStatus=0 才算 normal)
+                if (Objects.equals(status.getAlarmStatus(), 1)) alarm++;
+                else if (Objects.equals(status.getAlarmStatus(), 0)) normal++;
+                
+                // 统计:在线状态(onlineStatus=0 才算 offline)
+                if (Objects.equals(status.getOnlineStatus(), 1)) online++;
+                else if (Objects.equals(status.getOnlineStatus(), 0)) offline++;
+            } else {
+                // 没有状态记录的设备视为离线(不纳入报警/正常统计)
+                outDTO.setEquipmentStatus(null);
+                offline++;
+            }
+
+            // 坐标信息(来自设备基础表)
+            outDTO.setLongitude(device.getLongitude());
+            outDTO.setLatitude(device.getLatitude());
+
+            // 井盖监测数据
+            if (Boolean.TRUE.equals(listInDTO.getIsQueryManholeData())) {
+                List<ManholeData> dataList = imeiCardNumberMapList.get(device.getEquipmentCode());
+                if (CollUtil.isNotEmpty(dataList)) {
+                    ManholeData latestData = dataList.get(0);
+                    com.zksy.manhole.dto.out.ManholeDataOutDTO manholeDataOutDTO = new com.zksy.manhole.dto.out.ManholeDataOutDTO();
+                    BeanUtils.copyProperties(latestData, manholeDataOutDTO);
+                    outDTO.setManholeData(manholeDataOutDTO);
+                }
+            }
+
+            // 报警数据(最新一条)
+            List<AlarmData> alarms = alarmDataMap.get(device.getEquipmentCode());
+            if (CollUtil.isNotEmpty(alarms)) {
+                AlarmData latestAlarm = alarms.get(0);
+                outDTO.setAlarmData(latestAlarm);
+            }
+
+            outDTOList.add(outDTO);
+            total++;
+        }
+
+        statisticsMap.put("total", total);
+        statisticsMap.put("inUse", inUse);
+        statisticsMap.put("maintenance", maintenance);
+        statisticsMap.put("alarm", alarm);
+        statisticsMap.put("normal", normal);
+        statisticsMap.put("online", online);
+        statisticsMap.put("offline", offline);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("devices", outDTOList);
+        result.put("statistics", statisticsMap);
+        return result;
+    }
+
+
+
+}

+ 46 - 6
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/WorkOrderServiceImpl.java

@@ -27,10 +27,12 @@ import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 import java.util.Random;
+import java.util.stream.Collectors;
 
 /**
 * @author Administrator
@@ -55,16 +57,42 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     private EquipmentBaseMapper equipmentBaseMapper;
 
     /**
-     * 分页查询工单信息列表
+     * 查询窨井类型设备ID列表(equipment_type_id: 9=排水窨井, 10=供水窨井)
+     */
+    private List<String> getManholeDeviceIds() {
+        LambdaQueryWrapper<EquipmentBase> equipmentQuery = new LambdaQueryWrapper<>();
+        equipmentQuery.in(EquipmentBase::getEquipmentTypeId, Arrays.asList("5", "9", "10"));
+        return equipmentBaseMapper.selectList(equipmentQuery).stream()
+                .map(EquipmentBase::getEquipmentId)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 构造空分页结果
+     */
+    private Page<WorkOrderOutDTO> emptyPage(long pageNum, long pageSize) {
+        Page<WorkOrderOutDTO> emptyPage = new Page<>(pageNum, pageSize, 0);
+        emptyPage.setRecords(new ArrayList<>());
+        return emptyPage;
+    }
+
+    /**
+     * 分页查询工单信息列表(仅窨井类型设备)
      * @param pageInDTO
      * @return
      */
     @Override
     public Page<WorkOrderOutDTO> findByPage(WorkOrderPageInDTO pageInDTO) {
         log.info("分页查询工单信息列表-入参:{}", pageInDTO);
+        List<String> manholeDeviceIds = getManholeDeviceIds();
+        if (manholeDeviceIds.isEmpty()) {
+            return emptyPage(pageInDTO.getPageNum(), pageInDTO.getPageSize());
+        }
         Page<WorkOrder> page = new Page<>(pageInDTO.getPageNum(), pageInDTO.getPageSize());
         LambdaQueryWrapper<WorkOrder> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.like(StringUtils.isNotEmpty(pageInDTO.getOrderNo()), WorkOrder::getOrderNo, pageInDTO.getOrderNo())
+        queryWrapper.in(WorkOrder::getDeviceId, manholeDeviceIds)
+                .eq(StringUtils.isNotEmpty(pageInDTO.getDeviceId()), WorkOrder::getDeviceId, pageInDTO.getDeviceId())
+                .like(StringUtils.isNotEmpty(pageInDTO.getOrderNo()), WorkOrder::getOrderNo, pageInDTO.getOrderNo())
                 .like(StringUtils.isNotEmpty(pageInDTO.getCaseNo()), WorkOrder::getCaseNo, pageInDTO.getCaseNo())
                 .orderByDesc(WorkOrder::getUpdateTime);
         Page<WorkOrder> rs = this.page(page, queryWrapper);
@@ -86,6 +114,8 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
         workOrder.setOrderNo(generateOrderNo());
         // 初始状态:1-待派单
         workOrder.setOrderStatus(1);
+        workOrder.setCreateTime(new Date());
+        workOrder.setUpdateTime(new Date());
         if(inDTO.getCaseId() != null){
             CaseInfo caseInfo = caseInfoMapper.selectById(inDTO.getCaseId());
             if(caseInfo != null){
@@ -126,18 +156,23 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     }
 
     /**
-     * 经办工单分页查询(处理中:待接单、已接单、处理中、待验收)
+     * 经办工单分页查询(处理中:待接单、已接单、处理中、待验收,仅窨井类型设备
      * @param inDTO
      * @return
      */
     @Override
     public Page<WorkOrderOutDTO> findProcessingPage(WorkOrderProcessingInDTO inDTO) {
         log.info("经办工单分页查询-入参:{}", inDTO);
+        List<String> manholeDeviceIds = getManholeDeviceIds();
+        if (manholeDeviceIds.isEmpty()) {
+            return emptyPage(inDTO.getPageNum(), inDTO.getPageSize());
+        }
         Page<WorkOrder> page = new Page<>(inDTO.getPageNum(), inDTO.getPageSize());
         // 经办工单状态:2-待接单 3-已接单 4-处理中 5-待验收 8-延期审核
         List<Integer> processingStatusList = Arrays.asList(2, 3, 4, 5, 8);
         LambdaQueryWrapper<WorkOrder> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.in(WorkOrder::getOrderStatus, processingStatusList)
+        queryWrapper.in(WorkOrder::getDeviceId, manholeDeviceIds)
+                .in(WorkOrder::getOrderStatus, processingStatusList)
                 .like(StringUtils.isNotEmpty(inDTO.getOrderNo()), WorkOrder::getOrderNo, inDTO.getOrderNo())
                 .like(StringUtils.isNotEmpty(inDTO.getCaseNo()), WorkOrder::getCaseNo, inDTO.getCaseNo())
                 .eq(inDTO.getOrderType() != null, WorkOrder::getOrderType, inDTO.getOrderType())
@@ -152,18 +187,23 @@ public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder
     }
 
     /**
-     * 办结工单分页查询(已办结:已结案、已驳回)
+     * 办结工单分页查询(已办结:已结案、已驳回,仅窨井类型设备
      * @param inDTO
      * @return
      */
     @Override
     public Page<WorkOrderOutDTO> findFinishedPage(WorkOrderFinishedInDTO inDTO) {
         log.info("办结工单分页查询-入参:{}", inDTO);
+        List<String> manholeDeviceIds = getManholeDeviceIds();
+        if (manholeDeviceIds.isEmpty()) {
+            return emptyPage(inDTO.getPageNum(), inDTO.getPageSize());
+        }
         Page<WorkOrder> page = new Page<>(inDTO.getPageNum(), inDTO.getPageSize());
         // 办结工单状态:6-已结案 7-已驳回 9-已延期
         List<Integer> finishedStatusList = Arrays.asList(6, 7, 9);
         LambdaQueryWrapper<WorkOrder> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.in(WorkOrder::getOrderStatus, finishedStatusList)
+        queryWrapper.in(WorkOrder::getDeviceId, manholeDeviceIds)
+                .in(WorkOrder::getOrderStatus, finishedStatusList)
                 .like(StringUtils.isNotEmpty(inDTO.getOrderNo()), WorkOrder::getOrderNo, inDTO.getOrderNo())
                 .like(StringUtils.isNotEmpty(inDTO.getCaseNo()), WorkOrder::getCaseNo, inDTO.getCaseNo())
                 .eq(inDTO.getOrderType() != null, WorkOrder::getOrderType, inDTO.getOrderType())

+ 21 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/AlarmMonitorListInDTO.java

@@ -0,0 +1,21 @@
+package com.zksy.manhole.dto.in;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 窨井盖实时监测-设备报警监控列表入参
+ *
+ * @author xianggx
+ */
+@Data
+@ApiModel(value = "窨井盖实时监测-设备报警监控列表入参", description = "窨井盖实时监测-设备报警监控列表入参")
+public class AlarmMonitorListInDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "关键字,支持按设备编号、设备名称模糊查询")
+    private String keyword;
+}

+ 34 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/MaintenanceDataStatDashboardInDTO.java

@@ -0,0 +1,34 @@
+package com.zksy.manhole.dto.in;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 运维看板统计数据-入参
+ *
+ * @author xianggx
+ */
+@Data
+@ApiModel(value = "运维看板统计数据-入参", description = "运维看板统计数据-入参")
+public class MaintenanceDataStatDashboardInDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "趋势类型:1-周度 2-月度,默认月度")
+    private Integer trendType;
+
+    @ApiModelProperty(value = "统计开始日期")
+    @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date startDate;
+
+    @ApiModelProperty(value = "统计结束日期")
+    @JsonFormat(locale = "zh", pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date endDate;
+
+    @ApiModelProperty(value = "运维部门ID")
+    private Long deptId;
+}

+ 9 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/ManholeDeviceListInDTO.java

@@ -25,9 +25,18 @@ public class ManholeDeviceListInDTO implements Serializable {
     @ApiModelProperty(value = "在线状态(0-离线,1-在线)")
     private Integer onlineStatus;
 
+    @ApiModelProperty(value = "报警级别:0-有报警,1-严重报警,2-一般报警,不传则查询全部设备")
+    private Integer alarmLevel;
+
     @ApiModelProperty(value = "关键字,支持按设备编号、设备名称、设备位置模糊查询")
     private String keyword;
 
     @ApiModelProperty("是否查询井盖监测数据:默认不查询")
     private Boolean isQueryManholeData = false;
+
+    @ApiModelProperty(value = "当前记录起始索引,不传则不分页")
+    private Long pageNum;
+
+    @ApiModelProperty(value = "每页显示记录数,不传则不分页")
+    private Long pageSize;
 }

+ 24 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/ManholeDeviceStatisticsInDTO.java

@@ -0,0 +1,24 @@
+package com.zksy.manhole.dto.in;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 窨井设备统计(区域分布/类型分布)-入参
+ *
+ * @author xianggx
+ */
+@Data
+@ApiModel(value = "窨井设备统计-入参", description = "窨井设备统计-入参")
+public class ManholeDeviceStatisticsInDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "所属区域(片区),为空统计全部区域")
+    private String district;
+
+    @ApiModelProperty(value = "设备类型ID(9=排水窨井设备,10=供水窨井设备),为空统计全部类型")
+    private String equipmentTypeId;
+}

+ 3 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/in/WorkOrderPageInDTO.java

@@ -22,6 +22,9 @@ public class WorkOrderPageInDTO implements Serializable {
     @ApiModelProperty(value = "案件编号")
     private String caseNo;
 
+    @ApiModelProperty(value = "设备ID")
+    private String deviceId;
+
     @ApiModelProperty(value = "当前记录起始索引", required = true)
     private Long pageNum;
 

+ 26 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/AlarmMonitorListOutDTO.java

@@ -0,0 +1,26 @@
+package com.zksy.manhole.dto.out;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 窨井盖实时监测-设备报警监控列表出参(设备列表 + 统计信息)
+ *
+ * @author xianggx
+ */
+@Data
+@ApiModel(value = "窨井盖实时监测-设备报警监控列表出参", description = "设备列表 + 统计信息")
+public class AlarmMonitorListOutDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "设备列表(含最新 jg_device_data 记录)")
+    private List<EquipmentBaseOutDTO> devices;
+
+    @ApiModelProperty(value = "统计信息:total/critical/warning/normal/onlineCount/avgBattery")
+    private Map<String, Object> statistics;
+}

+ 7 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/EquipmentBaseOutDTO.java

@@ -1,5 +1,6 @@
 package com.zksy.manhole.dto.out;
 
+import com.zksy.base.alarm.domain.AlarmData;
 import com.zksy.base.domain.EquipmentBase;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
@@ -29,4 +30,10 @@ public class EquipmentBaseOutDTO extends EquipmentBase implements Serializable {
 
     @ApiModelProperty(value = "设备预警阈值列表")
     private List<WarningThresholdOutDTO> warningThresholdList;
+
+    @ApiModelProperty(value = "最新报警数据 (来自 alarm_data 表)")
+    private AlarmData alarmData;
+
+    @ApiModelProperty(value = "是否有未办结工单")
+    private Boolean workOrderSubmitted;
 }

+ 40 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/MaintenanceDataStatDashboardOutDTO.java

@@ -0,0 +1,40 @@
+package com.zksy.manhole.dto.out;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 运维看板统计数据-出参
+ * 一次返回页面全部业务数据:KPI指标、4类图表、异常终端列表
+ *
+ * @author xianggx
+ */
+@Data
+@ApiModel(value = "运维看板统计数据-出参", description = "运维看板统计数据-出参")
+public class MaintenanceDataStatDashboardOutDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "KPI指标:totalTerminal/abnormalTerminal/abnormalRate/submittedCount/solvedRate/totalCase/disposalRate/delayRate/avgDisposalTime")
+    private Map<String, Object> kpi;
+
+    @ApiModelProperty(value = "异常终端数量趋势(trendLabel/abnormalTerminal/totalTerminal)")
+    private List<Map<String, Object>> abnormalTerminalTrend = new ArrayList<>();
+
+    @ApiModelProperty(value = "案件处置率/延期率趋势(trendLabel/caseDisposalRate/caseDelayRate)")
+    private List<Map<String, Object>> caseRateTrend = new ArrayList<>();
+
+    @ApiModelProperty(value = "问题类型分布(name/value)")
+    private List<Map<String, Object>> problemTypeDistribution = new ArrayList<>();
+
+    @ApiModelProperty(value = "区域运维排行(name/total/abnormal)")
+    private List<Map<String, Object>> regionRank = new ArrayList<>();
+
+    @ApiModelProperty(value = "异常终端设备列表(含manholeData/equipmentStatus)")
+    private List<EquipmentBaseOutDTO> abnormalDevices = new ArrayList<>();
+}

+ 24 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/ManholeDataOutDTO.java

@@ -1,10 +1,12 @@
 package com.zksy.manhole.dto.out;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.util.Date;
 
 /**
  * 井盖实时监测数据-出参
@@ -16,12 +18,27 @@ import java.io.Serializable;
 public class ManholeDataOutDTO implements Serializable {
     private static final long serialVersionUID = 1L;
 
+    @ApiModelProperty(value = "主键ID")
+    private String id;
+
     @ApiModelProperty(value = "电池电量")
     private String batteryLevel;
 
+    @ApiModelProperty(value = "上传时间")
+    private String uploadTime;
+
+    @ApiModelProperty(value = "温度值")
+    private String temperatureValue;
+
     @ApiModelProperty(value = "信号量")
     private String signalStrength;
 
+    @ApiModelProperty(value = "综合报警状态(0正常,1报警)")
+    private String alarmStatus;
+
+    @ApiModelProperty(value = "角度报警阈值")
+    private String angleAlarmThreshold;
+
     @ApiModelProperty(value = "倾斜角度")
     private String tiltAngle;
 
@@ -33,4 +50,11 @@ public class ManholeDataOutDTO implements Serializable {
 
     @ApiModelProperty(value = "水位报警状态(0正常,1报警)")
     private String waterLevelAlarmStatus;
+
+    @ApiModelProperty(value = "IMEI 卡号")
+    private String imeiCardNumber;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createTime;
 }

+ 37 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/manhole/dto/out/ManholeLayerStatisticsOutDTO.java

@@ -0,0 +1,37 @@
+package com.zksy.manhole.dto.out;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 态势图层状态统计信息出参
+ */
+@Data
+@ApiModel(value = "态势图层状态统计", description = "态势图层状态统计")
+public class ManholeLayerStatisticsOutDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("设备总数")
+    private Integer total;
+
+    @ApiModelProperty("在用设备数量")
+    private Integer inUse;
+
+    @ApiModelProperty("维修设备数量")
+    private Integer maintenance;
+
+    @ApiModelProperty("报警设备数量")
+    private Integer alarm;
+
+    @ApiModelProperty("正常设备数量")
+    private Integer normal;
+
+    @ApiModelProperty("在线设备数量")
+    private Integer online;
+
+    @ApiModelProperty("离线/停用设备数量")
+    private Integer offline;
+}