LAPTOP-JI2IUVG1\26646 18 часов назад
Родитель
Сommit
c795753d0f
16 измененных файлов с 563 добавлено и 62 удалено
  1. 71 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/alarm/AlarmController.java
  2. 4 3
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/base/alarm/AlarmDataController.java
  3. 30 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/base/alarm/WarningThresholdController.java
  4. 7 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/risk/RiskController.java
  5. 16 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java
  6. 8 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/domain/WarningThreshold.java
  7. 3 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmDataService.java
  8. 14 3
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/AlarmDataServiceImpl.java
  9. 93 24
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/WarningThresholdServiceImpl.java
  10. 5 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/EquipmentBase.java
  11. 4 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/RiskAssessmentService.java
  12. 75 9
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/EquipmentBaseServiceImpl.java
  13. 90 15
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/GasDashboardServiceImpl.java
  14. 135 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/RiskAssessmentServiceImpl.java
  15. 3 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/DrainagePartition.java
  16. 5 2
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageWarningServiceImpl.java

+ 71 - 1
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/alarm/AlarmController.java

@@ -3,18 +3,25 @@ package com.zksy.web.controller.alarm;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 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.WorkOrder;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.service.BusinessWorkOrderService;
 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 com.zksy.common.utils.SecurityUtils;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
+import java.util.Map;
 
 @Slf4j
 @RestController
@@ -25,6 +32,12 @@ public class AlarmController {
     @Autowired
     private AlarmDataService service;
 
+    @Autowired
+    private BusinessWorkOrderService businessWorkOrderService;
+
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+
     @GetMapping("/findByPage")
     @ApiOperation("报警分页查询")
     @Anonymous
@@ -32,10 +45,67 @@ public class AlarmController {
                                   @ApiParam("每页条数") long pageSize,
                                   @RequestParam(required = false) String warningType,
                                   @RequestParam(required = false) Integer alarmStatus) {
-        Page<AlarmData> page = service.findByPage(pageNum, pageSize, null, warningType, null, alarmStatus, null);
+        Page<AlarmData> page = service.findByPage(pageNum, pageSize, null, warningType, null, alarmStatus, null, null, null);
         return AjaxResult.success(page);
     }
 
+    @PostMapping("/approve-to-work-order")
+    @ApiOperation("报警审核通过并创建运维工单")
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult approveToWorkOrder(@RequestBody Map<String, Object> body) {
+        if (body == null || body.get("alarmId") == null) {
+            return AjaxResult.error("报警ID不能为空");
+        }
+        String alarmId = body.get("alarmId").toString();
+        AlarmData alarm = service.getById(alarmId);
+        if (alarm == null) {
+            return AjaxResult.error("报警信息不存在");
+        }
+
+        long existing = businessWorkOrderService.lambdaQuery()
+                .eq(WorkOrder::getAlarmId, alarmId)
+                .count();
+        if (existing > 0) {
+            return AjaxResult.error("该报警已上传工单,不能重复创建");
+        }
+
+        EquipmentBase equipment = alarm.getDeviceCode() == null ? null
+                : equipmentBaseMapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EquipmentBase>()
+                        .eq(EquipmentBase::getEquipmentCode, alarm.getDeviceCode())
+                        .last("LIMIT 1"));
+        WorkOrder order = new WorkOrder();
+        order.setAlarmId(alarmId);
+        order.setDeviceId(equipment != null ? equipment.getEquipmentId() : alarm.getDeviceCode());
+        order.setDeviceCode(alarm.getDeviceCode());
+        order.setOrderType(1);
+        Object orderLevel = body.get("orderLevel");
+        order.setOrderLevel(orderLevel == null ? mapAlarmLevel(alarm.getAlarmLevel()) : Integer.valueOf(orderLevel.toString()));
+        Object orderDesc = body.get("orderDesc");
+        order.setOrderDesc(orderDesc == null || orderDesc.toString().trim().isEmpty()
+                ? buildOrderDesc(alarm) : orderDesc.toString());
+        Long orderId = businessWorkOrderService.createWorkOrder(order);
+
+        alarm.setAlarmStatus(1);
+        alarm.setHandleTime(LocalDateTime.now());
+        alarm.setHandleUser(SecurityUtils.getUsername());
+        alarm.setHandleRemark("审核通过,已上传工单:" + orderId);
+        alarm.setUpdateTime(LocalDateTime.now());
+        service.updateById(alarm);
+        return AjaxResult.success("审核通过,工单创建成功", orderId);
+    }
+
+    private Integer mapAlarmLevel(Integer alarmLevel) {
+        if (alarmLevel == null || alarmLevel == 1) return 1;
+        if (alarmLevel == 4) return 3;
+        return 2;
+    }
+
+    private String buildOrderDesc(AlarmData alarm) {
+        String value = alarm.getActualValue() == null ? "-" : alarm.getActualValue().toPlainString();
+        return (alarm.getWarningType() == null ? "监测报警" : alarm.getWarningType())
+                + ",当前值:" + value + ",请现场核实处理";
+    }
+
     @GetMapping("/getById/{id}")
     @ApiOperation("根据ID查询报警")
     @Anonymous

+ 4 - 3
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/base/alarm/AlarmDataController.java

@@ -29,10 +29,11 @@ public class AlarmDataController {
                                  @ApiParam(value = "预警类型", required = false) String warningType,
                                  @ApiParam(value = "预警编码", required = false) String warningCode,
                                  @ApiParam(value = "报警状态 0-未处理 1-已处理", required = false) Integer alarmStatus,
-                                 @ApiParam(value = "设备类型(如drainage)", required = false) String equipmentType) {
-        return AjaxResult.success(service.findByPage(pageNum, pageSize, deviceCode, warningType, warningCode, alarmStatus, equipmentType));
+                                 @ApiParam(value = "设备类型(如drainage)", required = false) String equipmentType,
+                                 @ApiParam(value = "开始时间(yyyy-MM-dd HH:mm:ss)", required = false) String startTime,
+                                 @ApiParam(value = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false) String endTime) {
+        return AjaxResult.success(service.findByPage(pageNum, pageSize, deviceCode, warningType, warningCode, alarmStatus, equipmentType, startTime, endTime));
     }
-
     @GetMapping("/getAlarmDataList")
     @ApiOperation(value = "报警数据信息查询")
     @Anonymous

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

@@ -46,7 +46,9 @@ public class WarningThresholdController {
                                        @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));
+        // 燃气阈值页面不再传模块名称;未传时按燃气设备处理。
+        String effectiveTypeName = org.springframework.util.StringUtils.hasText(typeName) ? typeName : "燃气";
+        return AjaxResult.success(service.findByModulePage(pageNum, pageSize, effectiveTypeName, deviceCode, warningType, warningCode));
     }
     @GetMapping("/getWarningThresholdList")
     @ApiOperation(value = "预警阈值信息查询")
@@ -66,6 +68,7 @@ public class WarningThresholdController {
     @ApiOperation(value = "预警阈值信息保存")
     public AjaxResult save(@RequestBody WarningThreshold entity,
                            @RequestParam(required = false) String moduleType) {
+        fillLegacyModuleFields(entity, moduleType);
         if (org.springframework.util.StringUtils.hasText(moduleType)
                 && !service.isDeviceInModule(entity.getDeviceCode(), moduleType)) {
             return AjaxResult.error("设备不属于指定模块,无法保存阈值");
@@ -76,6 +79,7 @@ public class WarningThresholdController {
     @ApiOperation(value = "预警阈值信息修改")
     public AjaxResult update(@RequestBody WarningThreshold entity,
                              @RequestParam(required = false) String moduleType) {
+        fillLegacyModuleFields(entity, moduleType);
         if (org.springframework.util.StringUtils.hasText(moduleType)
                 && !service.isDeviceInModule(entity.getDeviceCode(), moduleType)) {
             return AjaxResult.error("设备不属于指定模块,无法保存阈值");
@@ -83,6 +87,31 @@ public class WarningThresholdController {
         entity.setUpdateTime(LocalDateTime.now());
         return service.updateById(entity) ? AjaxResult.success(entity): AjaxResult.error("修改失败");
     }
+
+    /**
+     * 燃气、排水阈值页面不再使用预警类型和预警编码,但旧表可能将这两列定义为非空。
+     * 仅在接口层补齐兼容值,不作为查询条件,也不向前端展示。
+     */
+    private void fillLegacyModuleFields(WarningThreshold entity, String moduleType) {
+        if (entity == null) {
+            return;
+        }
+        if ("燃气".equals(moduleType)) {
+            if (!org.springframework.util.StringUtils.hasText(entity.getWarningType())) {
+                entity.setWarningType("燃气阈值");
+            }
+            if (!org.springframework.util.StringUtils.hasText(entity.getWarningCode())) {
+                entity.setWarningCode("GAS-THRESHOLD");
+            }
+        } else if ("排水".equals(moduleType)) {
+            if (!org.springframework.util.StringUtils.hasText(entity.getWarningType())) {
+                entity.setWarningType("排水阈值");
+            }
+            if (!org.springframework.util.StringUtils.hasText(entity.getWarningCode())) {
+                entity.setWarningCode("DRAINAGE-THRESHOLD");
+            }
+        }
+    }
     @PostMapping("/deleteBatch")
     @ApiOperation(value = "预警阈值信息删除")
     public AjaxResult delete(@RequestBody String[] ids) {

+ 7 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/risk/RiskController.java

@@ -69,4 +69,11 @@ public class RiskController {
     public AjaxResult getFourColorMap() {
         return AjaxResult.success(service.getFourColorMapData());
     }
+
+    @GetMapping("/gas-assessments")
+    @ApiOperation("燃气管网风险评估初版(基础信息+近30天报警)")
+    @Anonymous
+    public AjaxResult getGasAssessments() {
+        return AjaxResult.success(service.getGasRiskAssessments());
+    }
 }

+ 16 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/MonitorAlarm/service/impl/WaterSupplyAlarmServiceImpl.java

@@ -315,6 +315,13 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
             throw new ServiceException("报警信息不存在");
         }
 
+        // 同一报警只允许生成一张工单,避免重复审核/重复点击造成重复派单
+        long existingOrderCount = workOrderMapper.selectCount(new LambdaQueryWrapper<WorkOrder>()
+                .eq(WorkOrder::getAlarmId, alarm.getId()));
+        if (existingOrderCount > 0) {
+            throw new ServiceException("该报警已上传工单,不能重复创建");
+        }
+
         // 设备信息(用于回填 deviceId/deviceCode)
         EquipmentBase equipment = alarm.getDeviceCode() == null ? null
                 : equipmentBaseMapper.selectOne(new LambdaQueryWrapper<EquipmentBase>()
@@ -434,6 +441,15 @@ public class WaterSupplyAlarmServiceImpl implements IWaterSupplyAlarmService {
             alarm.setHandleRemark("误报解除:" + StrUtil.blankToDefault(inDTO.getAuditOpinion(), "经审核确认为误报"));
             alarm.setUpdateTime(LocalDateTime.now());
             alarmDataService.updateById(alarm);
+        } else {
+            // 审核通过即上传至运维工单,工单默认待派单状态
+            AlarmDispatchInDTO dispatch = new AlarmDispatchInDTO();
+            dispatch.setAlarmId(alarm.getId());
+            dispatch.setOrderType(1);
+            dispatch.setOrderLevel(mapAlarmLevelToOrderLevel(alarm.getAlarmLevel()));
+            dispatch.setOrderDesc(StrUtil.isBlank(inDTO.getAuditOpinion())
+                    ? null : inDTO.getAuditOpinion());
+            dispatchToWorkOrder(dispatch);
         }
         return toAuditOutDTO(audit);
     }

+ 8 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/domain/WarningThreshold.java

@@ -94,6 +94,14 @@ public class WarningThreshold implements Serializable {
     @ApiModelProperty(value = "修改时间")
     private LocalDateTime updateTime;
 
+    /** 设备表关联展示字段,不落库 */
+    @TableField(exist = false)
+    private String equipmentName;
+
+    /** 设备类型表关联展示字段,不落库 */
+    @TableField(exist = false)
+    private String equipmentTypeName;
+
     @TableField(exist = false)
     private static final long serialVersionUID = 1L;
 }

+ 3 - 4
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/AlarmDataService.java

@@ -7,11 +7,10 @@ import com.zksy.base.alarm.domain.AlarmData;
 import java.util.List;
 
 public interface AlarmDataService extends IService<AlarmData> {
-    Page<AlarmData> findByPage(long pageNum, long pageSize, String deviceCode, 
+    Page<AlarmData> findByPage(long pageNum, long pageSize, String deviceCode,
                                  String warningType, String warningCode, Integer alarmStatus,
-                                 String equipmentType);
-    
-    List<AlarmData> getAlarmDataList(String deviceCode, 
+                                 String equipmentType, String startTime, String endTime);
+        List<AlarmData> getAlarmDataList(String deviceCode, 
                                        String warningType, String warningCode, Integer alarmStatus);
     
     boolean saveAlarmData(AlarmData alarmData);

+ 14 - 3
pipe-network-service/zksy-system/src/main/java/com/zksy/base/alarm/service/impl/AlarmDataServiceImpl.java

@@ -25,7 +25,8 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
     @Override
     public Page<AlarmData> findByPage(long pageNum, long pageSize, String deviceCode,
                                         String warningType, String warningCode,
-                                        Integer alarmStatus, String equipmentType) {
+                                        Integer alarmStatus, String equipmentType,
+                                        String startTime, String endTime) {
         Page<AlarmData> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<AlarmData> queryWrapper = new LambdaQueryWrapper<>();
 
@@ -50,8 +51,14 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
         queryWrapper.like(deviceCode != null, AlarmData::getDeviceCode, deviceCode)
                 .like(warningType != null, AlarmData::getWarningType, warningType)
                 .like(warningCode != null, AlarmData::getWarningCode, warningCode)
-                .eq(alarmStatus != null, AlarmData::getAlarmStatus, alarmStatus)
-                .orderByDesc(AlarmData::getAlarmTime);
+                .eq(alarmStatus != null, AlarmData::getAlarmStatus, alarmStatus);
+        if (startTime != null && !startTime.isEmpty()) {
+            queryWrapper.ge(AlarmData::getAlarmTime, parseDateTime(startTime));
+        }
+        if (endTime != null && !endTime.isEmpty()) {
+            queryWrapper.le(AlarmData::getAlarmTime, parseDateTime(endTime));
+        }
+        queryWrapper.orderByDesc(AlarmData::getAlarmTime);
         Page<AlarmData> result = this.page(page, queryWrapper);
 
         // 填充 deviceName/longitude/latitude:通过 device_code 从 equipment_base 表关联查询
@@ -60,6 +67,10 @@ public class AlarmDataServiceImpl extends ServiceImpl<AlarmDataMapper, AlarmData
         return result;
     }
 
+    private LocalDateTime parseDateTime(String value) {
+        return LocalDateTime.parse(value, java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+    }
+
     /**
      * 根据 deviceCode 从 equipment_base 表批量填充 deviceName、longitude、latitude
      */

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

@@ -8,6 +8,8 @@ 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.domain.EquipmentType;
+import com.zksy.base.mapper.EquipmentTypeMapper;
 import com.zksy.base.service.EquipmentBaseService;
 import cn.hutool.core.collection.CollUtil;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -16,6 +18,10 @@ import org.springframework.stereotype.Service;
 import java.util.List;
 import java.util.Set;
 import java.util.Arrays;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.stream.Collectors;
 
 /**
@@ -30,6 +36,9 @@ public class WarningThresholdServiceImpl extends ServiceImpl<WarningThresholdMap
     @Autowired
     private EquipmentBaseService equipmentBaseService;
 
+    @Autowired
+    private EquipmentTypeMapper equipmentTypeMapper;
+
     @Override
     public Page<WarningThreshold> findByPage(long pageNum, long pageSize, String deviceCode, String warningType, String warningCode) {
         Page<WarningThreshold> page = new Page<>(pageNum, pageSize);
@@ -40,44 +49,104 @@ public class WarningThresholdServiceImpl extends ServiceImpl<WarningThresholdMap
         queryWrapper.like(WarningThreshold::getWarningType,warningType)
                 .like(WarningThreshold::getWarningCode,warningCode);
         queryWrapper.orderByDesc(WarningThreshold::getUpdateTime);
-        return this.page(page, queryWrapper);
+        Page<WarningThreshold> result = this.page(page, queryWrapper);
+        fillEquipmentInfo(result.getRecords(), equipmentBaseService.findByTopLevelType("燃气"));
+        return result;
     }
 
     @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;
+        if (CollUtil.isEmpty(equipmentList) && "燃气".equals(typeName)) {
+            equipmentList = equipmentBaseService.findByTopLevelType("燃气管网");
         }
-        List<String> deviceCodes = equipmentList.stream()
-                .map(EquipmentBase::getEquipmentCode)
-                .filter(code -> code != null && !code.isEmpty())
-                .distinct()
-                .collect(Collectors.toList());
-        if (CollUtil.isEmpty(deviceCodes)) {
+        if (CollUtil.isEmpty(equipmentList)) {
             return page;
         }
-        // 2. 查询阈值:device_code 字段匹配任意一个模块设备编码(支持逗号分隔多设备)
+        List<WarningThreshold> configured = new ArrayList<>();
         LambdaQueryWrapper<WarningThreshold> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.and(w -> {
-            for (String code : deviceCodes) {
-                w.or().apply("CONCAT(',', device_code, ',') LIKE CONCAT('%,', {0}, ',%')", code);
+        // 设备阈值配置按设备和设备类型管理,预警类型/编码不参与模块列表查询。
+        queryWrapper.orderByDesc(WarningThreshold::getUpdateTime);
+        configured = this.list(queryWrapper);
+        Map<String, WarningThreshold> thresholdByDevice = new HashMap<>();
+        for (WarningThreshold threshold : configured) {
+            if (threshold.getDeviceCode() == null) continue;
+            for (String code : threshold.getDeviceCode().split(",")) {
+                if (!code.trim().isEmpty()) thresholdByDevice.putIfAbsent(code.trim(), threshold);
             }
-        });
-        // 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);
+        List<WarningThreshold> rows = new ArrayList<>();
+        for (EquipmentBase equipment : equipmentList) {
+            String code = equipment.getEquipmentCode();
+            if (code == null || code.trim().isEmpty()) continue;
+            if (deviceCode != null && !deviceCode.isEmpty() && !code.contains(deviceCode)) continue;
+            WarningThreshold configuredRow = thresholdByDevice.get(code);
+            WarningThreshold row = configuredRow == null ? new WarningThreshold() : copyThreshold(configuredRow);
+            row.setDeviceCode(code);
+            row.setEquipmentName(equipment.getEquipmentName());
+            row.setEquipmentTypeName(equipment.getEquipmentTypeName());
+            rows.add(row);
         }
-        if (warningCode != null && !warningCode.isEmpty()) {
-            queryWrapper.like(WarningThreshold::getWarningCode, warningCode);
+        long total = rows.size();
+        int from = (int) Math.min(total, Math.max(0, (pageNum - 1) * pageSize));
+        int to = (int) Math.min(total, from + pageSize);
+        Page<WarningThreshold> result = new Page<>(pageNum, pageSize, total);
+        result.setRecords(from < to ? rows.subList(from, to) : Collections.emptyList());
+        return result;
+    }
+
+    private WarningThreshold copyThreshold(WarningThreshold source) {
+        WarningThreshold target = new WarningThreshold();
+        target.setId(source.getId());
+        target.setWarningType(source.getWarningType());
+        target.setWarningCode(source.getWarningCode());
+        target.setMinValue(source.getMinValue());
+        target.setMaxValue(source.getMaxValue());
+        target.setPeriodStart(source.getPeriodStart());
+        target.setPeriodEnd(source.getPeriodEnd());
+        target.setRemark(source.getRemark());
+        target.setCreateTime(source.getCreateTime());
+        target.setUpdateTime(source.getUpdateTime());
+        return target;
+    }
+
+    private void fillEquipmentInfo(List<WarningThreshold> records, List<EquipmentBase> equipmentList) {
+        if (CollUtil.isEmpty(records) || CollUtil.isEmpty(equipmentList)) return;
+        Map<String, String> typeNames = new HashMap<>();
+        Set<String> typeIds = equipmentList.stream().map(EquipmentBase::getEquipmentTypeId)
+                .filter(v -> v != null && !v.isEmpty()).collect(Collectors.toSet());
+        if (!typeIds.isEmpty()) {
+            for (EquipmentType type : equipmentTypeMapper.selectBatchIds(typeIds)) {
+                if (type.getId() != null && type.getTypeName() != null) typeNames.put(type.getId(), type.getTypeName());
+            }
+        }
+        Map<String, EquipmentBase> byCode = new HashMap<>();
+        for (EquipmentBase equipment : equipmentList) {
+            if (equipment.getEquipmentCode() != null) byCode.put(equipment.getEquipmentCode(), equipment);
+        }
+        for (WarningThreshold threshold : records) {
+            String[] codes = threshold.getDeviceCode() == null ? new String[0] : threshold.getDeviceCode().split(",");
+            StringBuilder names = new StringBuilder();
+            StringBuilder types = new StringBuilder();
+            for (String rawCode : codes) {
+                EquipmentBase equipment = byCode.get(rawCode.trim());
+                if (equipment == null) continue;
+                if (equipment.getEquipmentName() != null && names.indexOf(equipment.getEquipmentName()) < 0) {
+                    if (names.length() > 0) names.append("、");
+                    names.append(equipment.getEquipmentName());
+                }
+                String equipmentTypeName = equipment.getEquipmentTypeName();
+                if (equipmentTypeName == null) equipmentTypeName = typeNames.get(equipment.getEquipmentTypeId());
+                if (equipmentTypeName != null && types.indexOf(equipmentTypeName) < 0) {
+                    if (types.length() > 0) types.append("、");
+                    types.append(equipmentTypeName);
+                }
+            }
+            threshold.setEquipmentName(names.length() == 0 ? null : names.toString());
+            threshold.setEquipmentTypeName(types.length() == 0 ? null : types.toString());
         }
-        queryWrapper.orderByDesc(WarningThreshold::getUpdateTime);
-        return this.page(page, queryWrapper);
     }
 
     @Override

+ 5 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/EquipmentBase.java

@@ -2,6 +2,7 @@ package com.zksy.base.domain;
 
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModelProperty;
@@ -51,6 +52,10 @@ public class EquipmentBase {
     @ApiModelProperty("设备类别ID(关联equipment_type表)")
     private String equipmentTypeId;
 
+    @TableField(exist = false)
+    @ApiModelProperty("设备类别名称")
+    private String equipmentTypeName;
+
     @ApiModelProperty("设备资产价值(元)")
     private Double assetValue;
 

+ 4 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/RiskAssessmentService.java

@@ -7,10 +7,14 @@ import com.zksy.base.dto.RiskPageInDTO;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Map;
 
 public interface RiskAssessmentService extends IService<RiskAssessment> {
 
     Page<RiskAssessment> findByPage(RiskPageInDTO dto);
 
+    /** 基于管网基础信息和近30天报警数据计算燃气风险评估(初版模型) */
+    List<Map<String, Object>> getGasRiskAssessments();
+
     List<Map<String, Object>> getFourColorMapData();
 }

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

@@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 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.constant.GasEquipmentTypeEnum;
 import com.zksy.base.domain.*;
 import com.zksy.base.domain.vo.EquipmentFullVO;
 import com.zksy.base.event.DeviceStatusChangePublisher;
@@ -1169,19 +1170,68 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
 
     @Override
     public List<EquipmentBase> findByTopLevelType(String typeName) {
-        EquipmentType topType = equipmentTypeMapper.selectOne(
+        Set<String> typeIds = new LinkedHashSet<>();
+        Set<String> parentIds = new LinkedHashSet<>();
+
+        // 优先按名称查找顶级分类;使用 selectList 取第一条,避免脏数据导致 selectOne 多结果异常。
+        List<EquipmentType> topTypes = equipmentTypeMapper.selectList(
                 new LambdaQueryWrapper<EquipmentType>()
                         .eq(EquipmentType::getTypeName, typeName)
-                        .eq(EquipmentType::getParentTypeId, "0"));
-        if (topType == null) {
+                        .eq(EquipmentType::getParentTypeId, "0")
+                        .last("LIMIT 1"));
+        if (CollUtil.isNotEmpty(topTypes)) {
+            String topId = topTypes.get(0).getId();
+            if (StringUtils.isNotEmpty(topId)) {
+                typeIds.add(topId);
+                parentIds.add(topId);
+            }
+        }
+
+        // 燃气类型历史数据存在根节点名称不统一的情况,固定兼容 parent_type_id=3
+        // 及 id=3 的根节点,确保设备表中 equipment_type_id=3 或其后代都能查到。
+        if (StringUtils.isNotEmpty(typeName) && typeName.contains("燃气")) {
+            List<EquipmentType> gasRoots = equipmentTypeMapper.selectList(
+                    new LambdaQueryWrapper<EquipmentType>()
+                            .and(w -> w.eq(EquipmentType::getId, GasEquipmentTypeEnum.PARENT_TYPE_ID)
+                                    .or().eq(EquipmentType::getTypeId, GasEquipmentTypeEnum.PARENT_TYPE_ID))
+                            .last("LIMIT 1"));
+            if (CollUtil.isNotEmpty(gasRoots) && StringUtils.isNotEmpty(gasRoots.get(0).getId())) {
+                EquipmentType gasRoot = gasRoots.get(0);
+                typeIds.add(gasRoot.getId());
+                parentIds.add(gasRoot.getId());
+            }
+            List<EquipmentType> gasChildren = equipmentTypeMapper.selectList(
+                    new LambdaQueryWrapper<EquipmentType>()
+                            .eq(EquipmentType::getParentTypeId, GasEquipmentTypeEnum.PARENT_TYPE_ID));
+            for (EquipmentType child : gasChildren) {
+                if (child != null && StringUtils.isNotEmpty(child.getId())) {
+                    typeIds.add(child.getId());
+                    parentIds.add(child.getId());
+                }
+            }
+        }
+
+        // 排水模块的顶级名称在历史数据中可能不是“排水”,按稳定 type_id 兼容查询。
+        if (typeIds.isEmpty() && StringUtils.isNotEmpty(typeName) && typeName.contains("排水")) {
+            List<EquipmentType> drainageTypes = equipmentTypeMapper.selectList(
+                    new LambdaQueryWrapper<EquipmentType>()
+                            .eq(EquipmentType::getTypeId, "drainage")
+                            .eq(EquipmentType::getParentTypeId, "0")
+                            .last("LIMIT 1"));
+            if (CollUtil.isNotEmpty(drainageTypes)) {
+                String drainageId = drainageTypes.get(0).getId();
+                if (StringUtils.isNotEmpty(drainageId)) {
+                    typeIds.add(drainageId);
+                    parentIds.add(drainageId);
+                }
+            }
+        }
+
+        if (typeIds.isEmpty()) {
             return new ArrayList<>();
         }
 
-        // 设备类型可能存在多级子分类,查询整个模块的类型树,避免漏掉深层燃气设备。
-        Set<String> typeIds = new LinkedHashSet<>();
-        Set<String> parentIds = new LinkedHashSet<>();
-        typeIds.add(topType.getId());
-        parentIds.add(topType.getId());
+        // 设备类型可能存在多级子分类,查询整个模块的类型树,避免漏掉深层设备。
         while (!parentIds.isEmpty()) {
             List<EquipmentType> children = equipmentTypeMapper.selectList(
                     new LambdaQueryWrapper<EquipmentType>()
@@ -1197,6 +1247,22 @@ public class EquipmentBaseServiceImpl extends ServiceImpl<EquipmentBaseMapper, E
         LambdaQueryWrapper<EquipmentBase> wrapper = new LambdaQueryWrapper<>();
         wrapper.in(EquipmentBase::getEquipmentTypeId, new ArrayList<>(typeIds));
         wrapper.orderByDesc(EquipmentBase::getCreateTime);
-        return this.list(wrapper);
+        List<EquipmentBase> equipmentList = this.list(wrapper);
+
+        // 回填设备类型名称,供阈值配置、设备台账等页面直接展示。
+        Map<String, String> typeNameMap = new HashMap<>();
+        List<EquipmentType> equipmentTypes = equipmentTypeMapper.selectList(
+                new LambdaQueryWrapper<EquipmentType>().in(EquipmentType::getId, new ArrayList<>(typeIds)));
+        for (EquipmentType type : equipmentTypes) {
+            if (type != null && StringUtils.isNotEmpty(type.getId())) {
+                typeNameMap.put(type.getId(), type.getTypeName());
+            }
+        }
+        equipmentList.forEach(equipment -> {
+            String resolvedTypeName = typeNameMap.get(equipment.getEquipmentTypeId());
+            equipment.setEquipmentTypeName(StringUtils.isNotEmpty(resolvedTypeName)
+                    ? resolvedTypeName : equipment.getEquipmentTypeId());
+        });
+        return equipmentList;
     }
 }

+ 90 - 15
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/GasDashboardServiceImpl.java

@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.zksy.base.constant.GasEquipmentTypeEnum;
 import com.zksy.base.domain.*;
 import com.zksy.base.mapper.*;
+import com.zksy.base.gas.domain.GasMonitorData;
+import com.zksy.base.gas.mapper.GasMonitorDataMapper;
 import com.zksy.base.service.GasDashboardService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -21,6 +23,12 @@ public class GasDashboardServiceImpl implements GasDashboardService {
     @Autowired
     private GasPipePointMapper gasPipePointMapper;
 
+    @Autowired
+    private GasPipePointDeviceRelMapper gasPipePointDeviceRelMapper;
+
+    @Autowired
+    private GasMonitorDataMapper gasMonitorDataMapper;
+
     @Autowired
     private EquipmentBaseMapper equipmentBaseMapper;
 
@@ -123,12 +131,15 @@ public class GasDashboardServiceImpl implements GasDashboardService {
                 .count();
         // 今日告警
         String todayStr = LocalDate.now().toString();
-        long todayAlarm = equipmentStatusMapper.selectList(
-                new LambdaQueryWrapper<EquipmentStatus>()
-                        .in(EquipmentStatus::getEquipmentId, gasEquipmentIds)
-                        .eq(EquipmentStatus::getAlarmStatus, 1)).stream()
-                .filter(s -> s.getStatusUpdateTime() != null && s.getStatusUpdateTime().toString().startsWith(todayStr))
-                .count();
+        long todayAlarm = 0;
+        if (!gasEquipmentIds.isEmpty()) {
+            todayAlarm = equipmentStatusMapper.selectList(
+                    new LambdaQueryWrapper<EquipmentStatus>()
+                            .in(EquipmentStatus::getEquipmentId, gasEquipmentIds)
+                            .eq(EquipmentStatus::getAlarmStatus, 1)).stream()
+                    .filter(s -> s.getStatusUpdateTime() != null && s.getStatusUpdateTime().toString().startsWith(todayStr))
+                    .count();
+        }
 
         // 压力均值(从firefighting_pressure取最新一条)
         double avgPressure = 0;
@@ -230,22 +241,86 @@ public class GasDashboardServiceImpl implements GasDashboardService {
         result.put("hazardStats", levelGroup);
 
         // 6. 监测点数据
+        List<GasPipePointDeviceRel> pointDeviceRels = gasPipePointDeviceRelMapper.selectList(
+                new LambdaQueryWrapper<GasPipePointDeviceRel>()
+                        .eq(GasPipePointDeviceRel::getDelFlag, "0"));
+        Map<Long, List<GasPipePointDeviceRel>> relByPoint = pointDeviceRels.stream()
+                .filter(rel -> rel.getPointId() != null)
+                .collect(Collectors.groupingBy(GasPipePointDeviceRel::getPointId));
         List<Map<String, Object>> pointRows = new ArrayList<>();
-        for (int i = 0; i < Math.min(allPoints.size(), 8); i++) {
-            GasPipePoint p = allPoints.get(i);
-            Map<String, Object> row = new LinkedHashMap<>();
-            row.put("id", i + 1);
-            row.put("name", p.getPointName());
-            row.put("time", new Date().toString().substring(11, 16));
-            row.put("value", String.format("%.2fMPa", 0.15 + Math.random() * 0.1));
-            row.put("status", "0".equals(p.getStatus()) || "ACTIVE".equals(p.getStatus()) ? "在线" : "离线");
-            pointRows.add(row);
+        int rowIndex = 1;
+        for (GasPipePoint p : allPoints) {
+            List<GasPipePointDeviceRel> relations = relByPoint.getOrDefault(p.getPointId(), Collections.emptyList());
+            // 没有关联设备的监测点也保留一行,便于识别未配置设备的点位。
+            if (relations.isEmpty()) {
+                pointRows.add(buildPointRow(rowIndex++, p, null));
+                continue;
+            }
+            for (GasPipePointDeviceRel rel : relations) {
+                pointRows.add(buildPointRow(rowIndex++, p, rel));
+            }
         }
         result.put("pointRows", pointRows);
 
         return result;
     }
 
+    private Map<String, Object> buildPointRow(int id, GasPipePoint point, GasPipePointDeviceRel relation) {
+        Map<String, Object> row = new LinkedHashMap<>();
+        row.put("id", id);
+        row.put("name", point.getPointName());
+        row.put("pointName", point.getPointName());
+        row.put("pointCode", point.getPointCode());
+        String equipmentName = relation == null ? null : relation.getEquipmentName();
+        String equipmentCode = null;
+        String status = "0".equals(point.getStatus()) || "ACTIVE".equals(point.getStatus()) ? "在线" : "离线";
+        String value = "--";
+        String time = "--";
+        if (relation != null && relation.getEquipmentId() != null) {
+            EquipmentBase equipment = equipmentBaseMapper.selectById(relation.getEquipmentId());
+            if (equipment != null) {
+                equipmentName = equipmentName == null || equipmentName.isEmpty() ? equipment.getEquipmentName() : equipmentName;
+                equipmentCode = equipment.getEquipmentCode();
+                EquipmentStatus equipmentStatus = equipmentStatusMapper.selectOne(
+                        new LambdaQueryWrapper<EquipmentStatus>()
+                                .eq(EquipmentStatus::getEquipmentId, equipment.getEquipmentId())
+                                .orderByDesc(EquipmentStatus::getStatusUpdateTime)
+                                .last("LIMIT 1"));
+                if (equipmentStatus != null && equipmentStatus.getOnlineStatus() != null) {
+                    status = equipmentStatus.getOnlineStatus() == 1 ? "在线" : "离线";
+                }
+                GasMonitorData latest = gasMonitorDataMapper.selectOne(
+                        new LambdaQueryWrapper<GasMonitorData>()
+                                .eq(GasMonitorData::getMacAddress, equipment.getEquipmentCode())
+                                .orderByDesc(GasMonitorData::getReportTime)
+                                .last("LIMIT 1"));
+                if (latest == null && relation.getEquipmentId() != null) {
+                    latest = gasMonitorDataMapper.selectOne(
+                            new LambdaQueryWrapper<GasMonitorData>()
+                                    .eq(GasMonitorData::getMacAddress, relation.getEquipmentId())
+                                    .orderByDesc(GasMonitorData::getReportTime)
+                                    .last("LIMIT 1"));
+                }
+                if (latest != null) {
+                    if (latest.getGasConcentration() != null) {
+                        value = latest.getGasConcentration().stripTrailingZeros().toPlainString() + "%LEL";
+                    }
+                    if (latest.getReportTime() != null) {
+                        time = latest.getReportTime().toString().replace('T', ' ');
+                    } else if (latest.getCreateTime() != null) {
+                        time = latest.getCreateTime().toString().replace('T', ' ');
+                    }
+                }
+            }
+        }
+        row.put("deviceName", equipmentName == null ? "-" : equipmentName);
+        row.put("deviceCode", equipmentCode == null && relation != null ? relation.getEquipmentId() : equipmentCode);
+        row.put("time", time);
+        row.put("value", value);
+        row.put("status", status);
+        return row;
+    }
+
     private String statusLabel(Integer currentStatus) {
         if (currentStatus == null) return "正常";
         switch (currentStatus) {

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

@@ -5,6 +5,14 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.base.domain.PipeNetworkBase;
 import com.zksy.base.domain.RiskAssessment;
+import com.zksy.base.domain.GasPipePoint;
+import com.zksy.base.domain.GasPipePointDeviceRel;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.alarm.domain.AlarmData;
+import com.zksy.base.mapper.GasPipePointMapper;
+import com.zksy.base.mapper.GasPipePointDeviceRelMapper;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.alarm.mapper.AlarmDataMapper;
 import com.zksy.base.dto.RiskPageInDTO;
 import com.zksy.base.mapper.PipeNetworkBaseMapper;
 import com.zksy.base.mapper.RiskAssessmentMapper;
@@ -14,6 +22,10 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.math.BigDecimal;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -24,6 +36,18 @@ public class RiskAssessmentServiceImpl extends ServiceImpl<RiskAssessmentMapper,
     @Autowired
     private PipeNetworkBaseMapper pipeNetworkBaseMapper;
 
+    @Autowired
+    private GasPipePointMapper gasPipePointMapper;
+
+    @Autowired
+    private GasPipePointDeviceRelMapper gasPipePointDeviceRelMapper;
+
+    @Autowired
+    private EquipmentBaseMapper equipmentBaseMapper;
+
+    @Autowired
+    private AlarmDataMapper alarmDataMapper;
+
     @Override
     public Page<RiskAssessment> findByPage(RiskPageInDTO dto) {
         Page<RiskAssessment> page = new Page<>(dto.getPageNum(), dto.getPageSize());
@@ -72,4 +96,115 @@ public class RiskAssessmentServiceImpl extends ServiceImpl<RiskAssessmentMapper,
         }
         return result;
     }
+
+    @Override
+    public List<Map<String, Object>> getGasRiskAssessments() {
+        List<PipeNetworkBase> networks = pipeNetworkBaseMapper.selectList(
+                new LambdaQueryWrapper<PipeNetworkBase>()
+                        .and(w -> w.in(PipeNetworkBase::getNetworkType, "gas", "GAS", "Gas", "燃气", "燃气管网")
+                                .or().like(PipeNetworkBase::getNetworkName, "燃气"))
+                        .orderByDesc(PipeNetworkBase::getCreateTime));
+        if (networks.isEmpty()) return Collections.emptyList();
+
+        Set<String> networkIds = networks.stream().map(PipeNetworkBase::getNetworkId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        List<GasPipePoint> points = gasPipePointMapper.selectList(new LambdaQueryWrapper<GasPipePoint>()
+                .in(GasPipePoint::getNetworkId, networkIds));
+        Set<Long> pointIds = points.stream().map(GasPipePoint::getPointId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        List<GasPipePointDeviceRel> rels = pointIds.isEmpty() ? Collections.emptyList() :
+                gasPipePointDeviceRelMapper.selectList(new LambdaQueryWrapper<GasPipePointDeviceRel>()
+                        .in(GasPipePointDeviceRel::getPointId, pointIds)
+                        .eq(GasPipePointDeviceRel::getDelFlag, "0"));
+
+        Map<String, String> equipmentToNetwork = new HashMap<>();
+        Map<Long, String> pointNetwork = points.stream().collect(Collectors.toMap(
+                GasPipePoint::getPointId, GasPipePoint::getNetworkId, (a, b) -> a));
+        Set<String> equipmentIds = rels.stream().map(GasPipePointDeviceRel::getEquipmentId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        if (!equipmentIds.isEmpty()) {
+            List<EquipmentBase> equipment = equipmentBaseMapper.selectBatchIds(equipmentIds);
+            Map<String, String> idToCode = equipment.stream().collect(Collectors.toMap(
+                    EquipmentBase::getEquipmentId, EquipmentBase::getEquipmentCode, (a, b) -> a));
+            for (GasPipePointDeviceRel rel : rels) {
+                String code = idToCode.get(rel.getEquipmentId());
+                String networkId = pointNetwork.get(rel.getPointId());
+                if (code != null && networkId != null) equipmentToNetwork.put(code, networkId);
+            }
+        }
+
+        LocalDateTime since = LocalDateTime.now().minusDays(30);
+        List<AlarmData> alarms = equipmentToNetwork.isEmpty() ? Collections.emptyList() :
+                alarmDataMapper.selectList(new LambdaQueryWrapper<AlarmData>()
+                        .in(AlarmData::getDeviceCode, equipmentToNetwork.keySet())
+                        .ge(AlarmData::getAlarmTime, since));
+        Map<String, List<AlarmData>> alarmsByNetwork = new HashMap<>();
+        for (AlarmData alarm : alarms) {
+            String networkId = equipmentToNetwork.get(alarm.getDeviceCode());
+            if (networkId != null) alarmsByNetwork.computeIfAbsent(networkId, k -> new ArrayList<>()).add(alarm);
+        }
+
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (PipeNetworkBase pipe : networks) {
+            List<AlarmData> networkAlarms = alarmsByNetwork.getOrDefault(pipe.getNetworkId(), Collections.emptyList());
+            int ageScore = ageScore(pipe.getBuildDate());
+            int alarmScore = alarmFrequencyScore(networkAlarms.size());
+            int unresolvedScore = (int) Math.min(20, networkAlarms.stream()
+                    .filter(a -> Objects.equals(a.getAlarmStatus(), 0)).count() * 5);
+            int highLevelScore = (int) Math.min(15, networkAlarms.stream()
+                    .filter(a -> a.getAlarmLevel() != null && a.getAlarmLevel() <= 2).count() * 5);
+            int pressureScore = pressureScore(pipe.getNetworkLevel());
+            int materialScore = "钢管".equals(pipe.getMaterial()) ? 5 : 2;
+            int score = Math.min(100, ageScore + alarmScore + unresolvedScore + highLevelScore + pressureScore + materialScore);
+            String level = score >= 70 ? "high" : score >= 40 ? "medium" : "low";
+
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("id", pipe.getNetworkId());
+            item.put("networkId", pipe.getNetworkId());
+            item.put("code", pipe.getNetworkCode());
+            item.put("name", pipe.getNetworkName());
+            item.put("area", pipe.getLocation());
+            item.put("material", pipe.getMaterial());
+            item.put("diameter", pipe.getDiameter());
+            item.put("length", pipe.getLength() == null ? null : pipe.getLength().doubleValue() / 1000d);
+            item.put("buildYear", pipe.getBuildDate() == null ? null : pipe.getBuildDate().toInstant().atZone(ZoneId.systemDefault()).getYear());
+            item.put("pressure", pressureValue(pipe.getNetworkLevel()));
+            item.put("pressureLevel", pipe.getNetworkLevel());
+            item.put("owner", pipe.getManagementUnit());
+            item.put("lastInspection", null);
+            // 同步返回管网起终点坐标,当前端 GIS 图层接口暂时无法按编号匹配时,
+            // 仍可直接使用风险评估结果绘制管线。
+            item.put("startLongitude", pipe.getStartLongitude());
+            item.put("startLatitude", pipe.getStartLatitude());
+            item.put("endLongitude", pipe.getEndLongitude());
+            item.put("endLatitude", pipe.getEndLatitude());
+            item.put("riskLevel", level);
+            item.put("riskScore", score);
+            item.put("dimensions", dimensions(ageScore, alarmScore, unresolvedScore + highLevelScore, pressureScore, materialScore));
+            List<String> factors = new ArrayList<>();
+            if (ageScore >= 15) factors.add("管网使用年限较长(" + ageYears(pipe.getBuildDate()) + "年)");
+            if (!networkAlarms.isEmpty()) factors.add("近30天发生" + networkAlarms.size() + "次报警");
+            if (unresolvedScore > 0) factors.add("存在" + unresolvedScore / 5 + "条未处理报警");
+            if (highLevelScore > 0) factors.add("存在高等级报警");
+            if (factors.isEmpty()) factors.add("近30天无报警,基础资料风险较低");
+            item.put("riskFactors", factors);
+            item.put("mitigationMeasures", Arrays.asList("按风险等级安排巡检频次", "持续补充管网检测和维修记录"));
+            item.put("historyRecords", Collections.emptyList());
+            item.put("attachments", Collections.emptyList());
+            result.add(item);
+        }
+        return result;
+    }
+
+    private int ageYears(Date date) {
+        if (date == null) return 0;
+        return Math.max(0, LocalDate.now().getYear() - date.toInstant().atZone(ZoneId.systemDefault()).getYear());
+    }
+    private int ageScore(Date date) { int age = ageYears(date); return age == 0 ? 0 : age <= 5 ? 5 : age <= 10 ? 15 : age <= 15 ? 22 : 30; }
+    private int alarmFrequencyScore(int count) { return count == 0 ? 0 : count <= 2 ? 10 : count <= 5 ? 20 : 30; }
+    private int pressureScore(String level) { if (level == null) return 0; return level.contains("高") || level.toLowerCase().contains("high") ? 20 : level.contains("中") || level.toLowerCase().contains("medium") ? 12 : 5; }
+    private String pressureValue(String level) { if (level == null) return null; if (level.contains("高")) return "1.6"; if (level.contains("中")) return "0.4"; if (level.contains("低")) return "0.1"; return null; }
+    private Map<String, Integer> dimensions(int age, int alarms, int severity, int pressure, int material) {
+        Map<String, Integer> d = new LinkedHashMap<>(); d.put("leakProbability", Math.min(100, alarms * 3 + age * 2)); d.put("consequenceSeverity", Math.min(100, pressure * 4 + severity * 2)); d.put("corrosionRisk", Math.min(100, age * 3 + material * 4)); d.put("thirdPartyRisk", Math.min(100, alarms * 2 + pressure * 2)); d.put("agingLevel", Math.min(100, age * 3)); return d;
+    }
 }

+ 3 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/domain/DrainagePartition.java

@@ -50,6 +50,9 @@ public class DrainagePartition {
     @ApiModelProperty("纬度")
     private BigDecimal latitude;
 
+    @ApiModelProperty("分区面坐标JSON(百度坐标点数组)")
+    private String points;
+
     @ApiModelProperty("关联设备ID(多个用逗号分隔)")
     private String equipmentId;
 

+ 5 - 2
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageWarningServiceImpl.java

@@ -72,8 +72,11 @@ public class DrainageWarningServiceImpl extends ServiceImpl<DrainageWarningMappe
                                                   boolean historyOnly, String equipmentName) {
         Page<DrainageWarning> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<DrainageWarning> wrapper = new LambdaQueryWrapper<>();
-        // historyOnly=true 时不再自动添加 status IN (HISTORY_STATUSES) 条件
-        // 由前端通过 status 参数控制状态过滤,避免数据库 status 值格式不匹配导致查不到数据
+        // 历史查询默认只返回已处置或已解除的记录;传入 status 时允许
+        // 页面进一步限定其中一种状态。
+        if (historyOnly && (status == null || status.isEmpty())) {
+            wrapper.in(DrainageWarning::getStatus, HISTORY_STATUSES);
+        }
         // 附加筛选条件
         if (warningLevel != null) {
             wrapper.eq(DrainageWarning::getWarningLevel, warningLevel);