Procházet zdrojové kódy

Merge remote-tracking branch 'origin/master' into HEAD

Kazerin před 1 měsícem
rodič
revize
06ff6bb8c1
14 změnil soubory, kde provedl 622 přidání a 15 odebrání
  1. 13 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/DrainageEquipmentController.java
  2. 1 2
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/EngineeringFacilityController.java
  3. 98 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningIndustryAnalysisController.java
  4. 23 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/EarlyWarningMapper.java
  5. 24 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/IEarlyWarningIndustryAnalysisService.java
  6. 64 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningIndustryAnalysisServiceImpl.java
  7. 20 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/DrainageEquipmentBindingService.java
  8. 3 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/EngineeringFacilityService.java
  9. 182 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageEquipmentBindingServiceImpl.java
  10. 6 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainagePartitionServiceImpl.java
  11. 11 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/EngineeringFacilityServiceImpl.java
  12. 6 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/PipeNetworkInfoServiceImpl.java
  13. 6 4
      pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/WaterloggingPointServiceImpl.java
  14. 165 0
      pipe-network-service/zksy-system/src/main/resources/mapper/warning/EarlyWarningMapper.xml

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

@@ -13,6 +13,7 @@ import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.enums.BusinessType;
 import com.zksy.drainage.service.DrainageEquipmentPointRelService;
+import com.zksy.drainage.service.DrainageEquipmentBindingService;
 import com.zksy.drainage.service.DrainageEquipmentTestService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -52,8 +53,20 @@ public class DrainageEquipmentController {
     @Autowired
     private DrainageEquipmentPointRelService equipmentPointRelService;
 
+    @Autowired
+    private DrainageEquipmentBindingService equipmentBindingService;
+
     // ==================== 设备基础信息 ====================
 
+    @Anonymous
+    @GetMapping("/boundEquipmentIds")
+    @ApiOperation(value = "查询排水基础数据中已占用的设备")
+    public AjaxResult boundEquipmentIds(
+            @ApiParam("排除的对象类型") @RequestParam(value = "excludeType", required = false) String excludeType,
+            @ApiParam("排除的对象ID") @RequestParam(value = "excludeId", required = false) String excludeId) {
+        return AjaxResult.success(equipmentBindingService.findBoundEquipment(excludeType, excludeId));
+    }
+
     @Anonymous
     @GetMapping("/base/page")
     @ApiOperation(value = "监测设备分页查询")

+ 1 - 2
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/drainage/EngineeringFacilityController.java

@@ -124,8 +124,7 @@ public class EngineeringFacilityController {
         if (facilityId == null || pumpStationIds == null || pumpStationIds.isEmpty()) {
             return AjaxResult.error("参数不完整");
         }
-        // 复用bindEquipment,直接存储泵站ID到equipmentId字段,不做设备表校验
-        boolean success = service.bindEquipment(facilityId, pumpStationIds);
+        boolean success = service.bindPumpStation(facilityId, pumpStationIds);
         return success ? AjaxResult.success("关联成功") : AjaxResult.error("关联失败");
     }
 }

+ 98 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/warning/EarlyWarningIndustryAnalysisController.java

@@ -0,0 +1,98 @@
+package com.zksy.web.controller.warning;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.warning.service.IEarlyWarningIndustryAnalysisService;
+import com.zksy.common.core.domain.AjaxResult;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/warning/industryAnalysis")
+@Api(tags = "预警信息-行业处置分析模块")
+public class EarlyWarningIndustryAnalysisController {
+
+    private static final Logger log = LoggerFactory.getLogger(EarlyWarningIndustryAnalysisController.class);
+
+    @Autowired
+    private IEarlyWarningIndustryAnalysisService industryAnalysisService;
+
+    @GetMapping("/summary")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:industry')")
+    @ApiOperation("各行业预警处置总体情况")
+    public AjaxResult summary(
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
+            @RequestParam(required = false) String industryCode) {
+        try {
+            return AjaxResult.success(industryAnalysisService.getSummary(startTime, endTime, industryCode));
+        } catch (Exception e) {
+            log.error("行业处置汇总查询异常", e);
+            return AjaxResult.error("统计失败:" + e.getMessage());
+        }
+    }
+
+    @GetMapping("/status")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:industry')")
+    @ApiOperation("各行业预警处置状态分布")
+    public AjaxResult status(
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
+            @RequestParam(required = false) String industryCode) {
+        try {
+            return AjaxResult.success(industryAnalysisService.getStatusDistribution(startTime, endTime, industryCode));
+        } catch (Exception e) {
+            log.error("行业处置状态查询异常", e);
+            return AjaxResult.error("统计失败:" + e.getMessage());
+        }
+    }
+
+    @GetMapping("/efficiency")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:industry')")
+    @ApiOperation("各行业预警处置时效对比")
+    public AjaxResult efficiency(
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
+            @RequestParam(required = false) String industryCode) {
+        try {
+            return AjaxResult.success(industryAnalysisService.getEfficiencyComparison(startTime, endTime, industryCode));
+        } catch (Exception e) {
+            log.error("行业处置时效查询异常", e);
+            return AjaxResult.error("统计失败:" + e.getMessage());
+        }
+    }
+
+    @GetMapping("/page")
+    @PreAuthorize("@ss.hasPermi('warning:statistics:industry')")
+    @ApiOperation("行业预警处置分析明细")
+    public AjaxResult page(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime,
+            @RequestParam(required = false) String industryCode,
+            @RequestParam(required = false) String status,
+            @RequestParam(defaultValue = "desc") String sortType) {
+        try {
+            Page<Map<String, Object>> page = new Page<>(pageNum, pageSize);
+            IPage<Map<String, Object>> result = industryAnalysisService.queryPage(
+                    page, startTime, endTime, industryCode, status, sortType);
+            return AjaxResult.success(result);
+        } catch (Exception e) {
+            log.error("行业处置分析明细查询异常", e);
+            return AjaxResult.error("查询失败:" + e.getMessage());
+        }
+    }
+}

+ 23 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/mapper/EarlyWarningMapper.java

@@ -196,6 +196,29 @@ public interface EarlyWarningMapper extends BaseMapper<EarlyWarning> {
             @Param("status") String status,
             @Param("sortType") String sortType);
 
+    List<Map<String, Object>> selectIndustryAnalysisSummary(
+            @Param("startTime") LocalDateTime startTime,
+            @Param("endTime") LocalDateTime endTime,
+            @Param("industryCode") String industryCode);
+
+    List<Map<String, Object>> selectIndustryStatusDistribution(
+            @Param("startTime") LocalDateTime startTime,
+            @Param("endTime") LocalDateTime endTime,
+            @Param("industryCode") String industryCode);
+
+    List<Map<String, Object>> selectIndustryEfficiencyComparison(
+            @Param("startTime") LocalDateTime startTime,
+            @Param("endTime") LocalDateTime endTime,
+            @Param("industryCode") String industryCode);
+
+    IPage<Map<String, Object>> selectIndustryAnalysisList(
+            Page<Map<String, Object>> page,
+            @Param("startTime") LocalDateTime startTime,
+            @Param("endTime") LocalDateTime endTime,
+            @Param("industryCode") String industryCode,
+            @Param("status") String status,
+            @Param("sortType") String sortType);
+
     /**
      * Warning overview: current active warnings, warnings resolved today and
      * warnings published today. Drafts and unsupported facility types are excluded.

+ 24 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/IEarlyWarningIndustryAnalysisService.java

@@ -0,0 +1,24 @@
+package com.zksy.base.warning.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+public interface IEarlyWarningIndustryAnalysisService {
+
+    List<Map<String, Object>> getSummary(LocalDateTime startTime, LocalDateTime endTime, String industryCode);
+
+    List<Map<String, Object>> getStatusDistribution(LocalDateTime startTime, LocalDateTime endTime, String industryCode);
+
+    List<Map<String, Object>> getEfficiencyComparison(LocalDateTime startTime, LocalDateTime endTime, String industryCode);
+
+    IPage<Map<String, Object>> queryPage(Page<Map<String, Object>> page,
+                                         LocalDateTime startTime,
+                                         LocalDateTime endTime,
+                                         String industryCode,
+                                         String status,
+                                         String sortType);
+}

+ 64 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/warning/service/impl/EarlyWarningIndustryAnalysisServiceImpl.java

@@ -0,0 +1,64 @@
+package com.zksy.base.warning.service.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.warning.mapper.EarlyWarningMapper;
+import com.zksy.base.warning.service.IEarlyWarningIndustryAnalysisService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Service
+public class EarlyWarningIndustryAnalysisServiceImpl implements IEarlyWarningIndustryAnalysisService {
+
+    private static final Set<String> SUPPORTED_INDUSTRIES = Set.of("water_supply", "drainage", "gas", "manhole");
+
+    @Autowired
+    private EarlyWarningMapper earlyWarningMapper;
+
+    @Override
+    public List<Map<String, Object>> getSummary(LocalDateTime startTime, LocalDateTime endTime, String industryCode) {
+        return earlyWarningMapper.selectIndustryAnalysisSummary(startTime, endTime, normalizeIndustry(industryCode));
+    }
+
+    @Override
+    public List<Map<String, Object>> getStatusDistribution(LocalDateTime startTime, LocalDateTime endTime, String industryCode) {
+        return earlyWarningMapper.selectIndustryStatusDistribution(startTime, endTime, normalizeIndustry(industryCode));
+    }
+
+    @Override
+    public List<Map<String, Object>> getEfficiencyComparison(LocalDateTime startTime, LocalDateTime endTime, String industryCode) {
+        return earlyWarningMapper.selectIndustryEfficiencyComparison(startTime, endTime, normalizeIndustry(industryCode));
+    }
+
+    @Override
+    public IPage<Map<String, Object>> queryPage(Page<Map<String, Object>> page,
+                                                LocalDateTime startTime,
+                                                LocalDateTime endTime,
+                                                String industryCode,
+                                                String status,
+                                                String sortType) {
+        String safeSortType = "asc".equalsIgnoreCase(sortType) ? "asc" : "desc";
+        return earlyWarningMapper.selectIndustryAnalysisList(page, startTime, endTime,
+                normalizeIndustry(industryCode), normalizeStatus(status), safeSortType);
+    }
+
+    private String normalizeIndustry(String industryCode) {
+        if (industryCode == null || industryCode.trim().isEmpty()) {
+            return null;
+        }
+        String normalized = industryCode.trim().toLowerCase();
+        return SUPPORTED_INDUSTRIES.contains(normalized) ? normalized : null;
+    }
+
+    private String normalizeStatus(String status) {
+        if (status == null || status.trim().isEmpty()) {
+            return null;
+        }
+        return status.trim().toUpperCase();
+    }
+}

+ 20 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/DrainageEquipmentBindingService.java

@@ -0,0 +1,20 @@
+package com.zksy.drainage.service;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 排水基础数据设备关联服务。
+ * 同一监测设备在排水分区、排水管网、工程设施和易积水点中只能关联一个对象。
+ */
+public interface DrainageEquipmentBindingService {
+
+    String TYPE_PARTITION = "partition";
+    String TYPE_PIPE_NETWORK = "pipeNetwork";
+    String TYPE_FACILITY = "facility";
+    String TYPE_WATERLOGGING = "waterlogging";
+
+    Map<String, String> findBoundEquipment(String excludeType, String excludeId);
+
+    boolean bindEquipment(String sourceType, String sourceId, List<String> equipmentIds);
+}

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

@@ -41,4 +41,7 @@ public interface EngineeringFacilityService extends IService<EngineeringFacility
      * 绑定设备到工程设施
      */
     boolean bindEquipment(String id, List<String> equipmentIds);
+
+    /** 关联排水泵站;泵站不是监测设备,不参与设备唯一占用校验。 */
+    boolean bindPumpStation(String id, List<String> pumpStationIds);
 }

+ 182 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainageEquipmentBindingServiceImpl.java

@@ -0,0 +1,182 @@
+package com.zksy.drainage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.drainage.domain.DrainagePartition;
+import com.zksy.drainage.domain.EngineeringFacility;
+import com.zksy.drainage.domain.PipeNetworkInfo;
+import com.zksy.drainage.domain.WaterloggingPoint;
+import com.zksy.drainage.mapper.DrainagePartitionMapper;
+import com.zksy.drainage.mapper.EngineeringFacilityMapper;
+import com.zksy.drainage.mapper.PipeNetworkInfoMapper;
+import com.zksy.drainage.mapper.WaterloggingPointMapper;
+import com.zksy.drainage.service.DrainageEquipmentBindingService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** 排水设备全局唯一关联实现。 */
+@Service
+public class DrainageEquipmentBindingServiceImpl implements DrainageEquipmentBindingService {
+
+    @Autowired
+    private DrainagePartitionMapper drainagePartitionMapper;
+
+    @Autowired
+    private PipeNetworkInfoMapper pipeNetworkInfoMapper;
+
+    @Autowired
+    private EngineeringFacilityMapper engineeringFacilityMapper;
+
+    @Autowired
+    private WaterloggingPointMapper waterloggingPointMapper;
+
+    @Override
+    public Map<String, String> findBoundEquipment(String excludeType, String excludeId) {
+        validateSourceType(excludeType, true);
+        Map<String, String> result = new LinkedHashMap<>();
+
+        for (DrainagePartition item : drainagePartitionMapper.selectList(
+                new LambdaQueryWrapper<DrainagePartition>().select(
+                        DrainagePartition::getId, DrainagePartition::getName, DrainagePartition::getEquipmentId))) {
+            addBindings(result, TYPE_PARTITION, item.getId(), item.getName(), item.getEquipmentId(),
+                    excludeType, excludeId, "排水分区");
+        }
+        for (PipeNetworkInfo item : pipeNetworkInfoMapper.selectList(
+                new LambdaQueryWrapper<PipeNetworkInfo>().select(
+                        PipeNetworkInfo::getId, PipeNetworkInfo::getName, PipeNetworkInfo::getEquipmentId))) {
+            addBindings(result, TYPE_PIPE_NETWORK, item.getId(), item.getName(), item.getEquipmentId(),
+                    excludeType, excludeId, "排水管网");
+        }
+        for (EngineeringFacility item : engineeringFacilityMapper.selectList(
+                new LambdaQueryWrapper<EngineeringFacility>().select(
+                        EngineeringFacility::getId, EngineeringFacility::getName,
+                        EngineeringFacility::getFacilityType, EngineeringFacility::getEquipmentId))) {
+            // 泵站设施的 equipment_id 保存的是泵站ID,不属于监测设备关联。
+            if ("泵站".equals(item.getFacilityType())) {
+                continue;
+            }
+            addBindings(result, TYPE_FACILITY, item.getId(), item.getName(), item.getEquipmentId(),
+                    excludeType, excludeId, "工程设施");
+        }
+        for (WaterloggingPoint item : waterloggingPointMapper.selectList(
+                new LambdaQueryWrapper<WaterloggingPoint>().select(
+                        WaterloggingPoint::getId, WaterloggingPoint::getName, WaterloggingPoint::getEquipmentId))) {
+            addBindings(result, TYPE_WATERLOGGING, item.getId(), item.getName(), item.getEquipmentId(),
+                    excludeType, excludeId, "易积水点");
+        }
+        return result;
+    }
+
+    /** 同一个服务实例串行完成检查和保存,防止单实例内并发抢占同一设备。 */
+    @Override
+    @Transactional
+    public synchronized boolean bindEquipment(String sourceType, String sourceId, List<String> equipmentIds) {
+        validateSourceType(sourceType, false);
+        if (sourceId == null || sourceId.trim().isEmpty()) {
+            throw new ServiceException("关联对象ID不能为空");
+        }
+
+        List<String> normalizedIds = normalizeEquipmentIds(equipmentIds);
+        if (normalizedIds.isEmpty()) {
+            throw new ServiceException("请选择要关联的设备");
+        }
+
+        Map<String, String> occupied = findBoundEquipment(sourceType, sourceId);
+        List<String> conflicts = new ArrayList<>();
+        for (String equipmentId : normalizedIds) {
+            String sourceName = occupied.get(equipmentId);
+            if (sourceName != null) {
+                conflicts.add(equipmentId + "(" + sourceName + ")");
+            }
+        }
+        if (!conflicts.isEmpty()) {
+            throw new ServiceException("设备已被其他位置关联,不能重复关联:" + String.join("、", conflicts));
+        }
+
+        String value = String.join(",", normalizedIds);
+        switch (sourceType) {
+            case TYPE_PARTITION:
+                DrainagePartition partition = drainagePartitionMapper.selectById(sourceId);
+                if (partition == null) return false;
+                partition.setEquipmentId(value);
+                return drainagePartitionMapper.updateById(partition) > 0;
+            case TYPE_PIPE_NETWORK:
+                PipeNetworkInfo pipeNetwork = pipeNetworkInfoMapper.selectById(sourceId);
+                if (pipeNetwork == null) return false;
+                pipeNetwork.setEquipmentId(value);
+                return pipeNetworkInfoMapper.updateById(pipeNetwork) > 0;
+            case TYPE_FACILITY:
+                EngineeringFacility facility = engineeringFacilityMapper.selectById(sourceId);
+                if (facility == null) return false;
+                if ("泵站".equals(facility.getFacilityType())) {
+                    throw new ServiceException("泵站设施请使用关联泵站功能");
+                }
+                facility.setEquipmentId(value);
+                return engineeringFacilityMapper.updateById(facility) > 0;
+            case TYPE_WATERLOGGING:
+                WaterloggingPoint waterloggingPoint = waterloggingPointMapper.selectById(sourceId);
+                if (waterloggingPoint == null) return false;
+                waterloggingPoint.setEquipmentId(value);
+                return waterloggingPointMapper.updateById(waterloggingPoint) > 0;
+            default:
+                return false;
+        }
+    }
+
+    private void addBindings(Map<String, String> result, String sourceType, String sourceId,
+                             String sourceName, String equipmentIds, String excludeType,
+                             String excludeId, String sourceLabel) {
+        if (sourceType.equals(excludeType) && sourceId != null && sourceId.equals(excludeId)) {
+            return;
+        }
+        for (String equipmentId : splitEquipmentIds(equipmentIds)) {
+            String displayName = sourceName == null || sourceName.trim().isEmpty() ? sourceId : sourceName;
+            result.putIfAbsent(equipmentId, sourceLabel + " / " + displayName);
+        }
+    }
+
+    private List<String> normalizeEquipmentIds(List<String> equipmentIds) {
+        Set<String> result = new LinkedHashSet<>();
+        if (equipmentIds != null) {
+            for (String equipmentId : equipmentIds) {
+                if (equipmentId != null && !equipmentId.trim().isEmpty()) {
+                    result.add(equipmentId.trim());
+                }
+            }
+        }
+        return new ArrayList<>(result);
+    }
+
+    private List<String> splitEquipmentIds(String equipmentIds) {
+        List<String> result = new ArrayList<>();
+        if (equipmentIds == null || equipmentIds.trim().isEmpty()) {
+            return result;
+        }
+        for (String equipmentId : equipmentIds.split(",")) {
+            if (!equipmentId.trim().isEmpty()) {
+                result.add(equipmentId.trim());
+            }
+        }
+        return result;
+    }
+
+    private void validateSourceType(String sourceType, boolean allowEmpty) {
+        if (allowEmpty && (sourceType == null || sourceType.trim().isEmpty())) {
+            return;
+        }
+        if (!TYPE_PARTITION.equals(sourceType)
+                && !TYPE_PIPE_NETWORK.equals(sourceType)
+                && !TYPE_FACILITY.equals(sourceType)
+                && !TYPE_WATERLOGGING.equals(sourceType)) {
+            throw new ServiceException("不支持的排水设备关联类型:" + sourceType);
+        }
+    }
+}

+ 6 - 4
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/DrainagePartitionServiceImpl.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.common.exception.ServiceException;
 import com.zksy.drainage.domain.DrainagePartition;
 import com.zksy.drainage.mapper.DrainagePartitionMapper;
+import com.zksy.drainage.service.DrainageEquipmentBindingService;
 import com.zksy.drainage.service.DrainagePartitionService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -23,6 +24,9 @@ import java.util.stream.Collectors;
 public class DrainagePartitionServiceImpl extends ServiceImpl<DrainagePartitionMapper, DrainagePartition>
         implements DrainagePartitionService {
 
+    @org.springframework.beans.factory.annotation.Autowired
+    private DrainageEquipmentBindingService equipmentBindingService;
+
     @Override
     public Page<DrainagePartition> findByPage(long pageNum, long pageSize,
                                               String name, String district, String partitionType) {
@@ -104,9 +108,7 @@ public class DrainagePartitionServiceImpl extends ServiceImpl<DrainagePartitionM
 
     @Override
     public boolean bindEquipment(String partitionId, List<String> equipmentIds) {
-        DrainagePartition partition = this.getById(partitionId);
-        if (partition == null) return false;
-        partition.setEquipmentId(String.join(",", equipmentIds));
-        return this.updateById(partition);
+        return equipmentBindingService.bindEquipment(
+                DrainageEquipmentBindingService.TYPE_PARTITION, partitionId, equipmentIds);
     }
 }

+ 11 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/EngineeringFacilityServiceImpl.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.common.exception.ServiceException;
 import com.zksy.drainage.domain.EngineeringFacility;
 import com.zksy.drainage.mapper.EngineeringFacilityMapper;
+import com.zksy.drainage.service.DrainageEquipmentBindingService;
 import com.zksy.drainage.service.EngineeringFacilityService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -24,6 +25,9 @@ import java.util.stream.Collectors;
 public class EngineeringFacilityServiceImpl extends ServiceImpl<EngineeringFacilityMapper, EngineeringFacility>
         implements EngineeringFacilityService {
 
+    @org.springframework.beans.factory.annotation.Autowired
+    private DrainageEquipmentBindingService equipmentBindingService;
+
     @Override
     public Page<EngineeringFacility> findByPage(long pageNum, long pageSize,
                                                 String name, String area, String facilityType) {
@@ -94,9 +98,15 @@ public class EngineeringFacilityServiceImpl extends ServiceImpl<EngineeringFacil
 
     @Override
     public boolean bindEquipment(String id, List<String> equipmentIds) {
+        return equipmentBindingService.bindEquipment(
+                DrainageEquipmentBindingService.TYPE_FACILITY, id, equipmentIds);
+    }
+
+    @Override
+    public boolean bindPumpStation(String id, List<String> pumpStationIds) {
         EngineeringFacility entity = this.getById(id);
         if (entity == null) return false;
-        entity.setEquipmentId(String.join(",", equipmentIds));
+        entity.setEquipmentId(String.join(",", pumpStationIds));
         return this.updateById(entity);
     }
 }

+ 6 - 4
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/PipeNetworkInfoServiceImpl.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.common.exception.ServiceException;
 import com.zksy.drainage.domain.PipeNetworkInfo;
 import com.zksy.drainage.mapper.PipeNetworkInfoMapper;
+import com.zksy.drainage.service.DrainageEquipmentBindingService;
 import com.zksy.drainage.service.PipeNetworkInfoService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -24,6 +25,9 @@ import java.util.stream.Collectors;
 public class PipeNetworkInfoServiceImpl extends ServiceImpl<PipeNetworkInfoMapper, PipeNetworkInfo>
         implements PipeNetworkInfoService {
 
+    @org.springframework.beans.factory.annotation.Autowired
+    private DrainageEquipmentBindingService equipmentBindingService;
+
     @Override
     public Page<PipeNetworkInfo> findByPage(long pageNum, long pageSize,
                                             String name, String area, String networkType) {
@@ -94,9 +98,7 @@ public class PipeNetworkInfoServiceImpl extends ServiceImpl<PipeNetworkInfoMappe
 
     @Override
     public boolean bindEquipment(String id, List<String> equipmentIds) {
-        PipeNetworkInfo entity = this.getById(id);
-        if (entity == null) return false;
-        entity.setEquipmentId(String.join(",", equipmentIds));
-        return this.updateById(entity);
+        return equipmentBindingService.bindEquipment(
+                DrainageEquipmentBindingService.TYPE_PIPE_NETWORK, id, equipmentIds);
     }
 }

+ 6 - 4
pipe-network-service/zksy-system/src/main/java/com/zksy/drainage/service/impl/WaterloggingPointServiceImpl.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zksy.common.exception.ServiceException;
 import com.zksy.drainage.domain.WaterloggingPoint;
 import com.zksy.drainage.mapper.WaterloggingPointMapper;
+import com.zksy.drainage.service.DrainageEquipmentBindingService;
 import com.zksy.drainage.service.WaterloggingPointService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -23,6 +24,9 @@ import java.util.stream.Collectors;
 public class WaterloggingPointServiceImpl extends ServiceImpl<WaterloggingPointMapper, WaterloggingPoint>
         implements WaterloggingPointService {
 
+    @org.springframework.beans.factory.annotation.Autowired
+    private DrainageEquipmentBindingService equipmentBindingService;
+
     @Override
     public Page<WaterloggingPoint> findByPage(long pageNum, long pageSize, String name, String area) {
         Page<WaterloggingPoint> page = new Page<>(pageNum, pageSize);
@@ -80,9 +84,7 @@ public class WaterloggingPointServiceImpl extends ServiceImpl<WaterloggingPointM
 
     @Override
     public boolean bindEquipment(String id, List<String> equipmentIds) {
-        WaterloggingPoint entity = this.getById(id);
-        if (entity == null) return false;
-        entity.setEquipmentId(String.join(",", equipmentIds));
-        return this.updateById(entity);
+        return equipmentBindingService.bindEquipment(
+                DrainageEquipmentBindingService.TYPE_WATERLOGGING, id, equipmentIds);
     }
 }

+ 165 - 0
pipe-network-service/zksy-system/src/main/resources/mapper/warning/EarlyWarningMapper.xml

@@ -993,4 +993,169 @@
         FROM early_warning
     </select>
 
+    <!-- 行业处置分析:兼容 device_code 存设备实例编码或设备类别业务编码两种历史格式。 -->
+    <sql id="IndustryWarningCte">
+        WITH RECURSIVE type_tree AS (
+            SELECT et.id AS leaf_id, et.id AS current_id, et.type_id AS current_type_id,
+                   et.type_name AS current_name, et.parent_type_id, CAST(0 AS bigint) AS depth
+            FROM app_user.equipment_type et
+            UNION ALL
+            SELECT tree.leaf_id, parent.id, parent.type_id, parent.type_name,
+                   parent.parent_type_id, tree.depth + 1
+            FROM type_tree tree
+            INNER JOIN app_user.equipment_type parent ON parent.id = tree.parent_type_id
+        ),
+        root_ranked AS (
+            SELECT leaf_id, current_type_id AS industry_code, current_name AS industry_name,
+                   ROW_NUMBER() OVER (PARTITION BY leaf_id ORDER BY depth DESC) AS row_num
+            FROM type_tree
+        ),
+        type_roots AS (
+            SELECT leaf_id, industry_code, industry_name
+            FROM root_ranked
+            WHERE row_num = 1
+              AND industry_code IN ('water_supply', 'drainage', 'gas', 'manhole')
+        ),
+        equipment_map AS (
+            SELECT equipment_code, MIN(equipment_type_id) AS equipment_type_id
+            FROM app_user.equipment_base
+            WHERE del_flag = '0'
+            GROUP BY equipment_code
+        ),
+        type_code_map AS (
+            SELECT type_id, MIN(id) AS id
+            FROM app_user.equipment_type
+            GROUP BY type_id
+        ),
+        warning_industry AS (
+            SELECT w.*, roots.industry_code, roots.industry_name
+            FROM app_user.early_warning w
+            LEFT JOIN equipment_map equipment ON equipment.equipment_code = w.device_code
+            LEFT JOIN type_code_map direct_type ON direct_type.type_id = w.device_code
+            INNER JOIN type_roots roots
+                    ON roots.leaf_id = COALESCE(equipment.equipment_type_id, direct_type.id)
+            WHERE w.status != 'DRAFT'
+            <if test="startTime != null">
+                AND w.publish_time &gt;= #{startTime}
+            </if>
+            <if test="endTime != null">
+                AND w.publish_time &lt; #{endTime}
+            </if>
+            <if test="industryCode != null and industryCode != ''">
+                AND roots.industry_code = #{industryCode}
+            </if>
+        ),
+        completed_actions AS (
+            SELECT disposal.warning_id, MAX(disposal.create_time) AS dispose_time
+            FROM app_user.warning_disposal disposal
+            WHERE disposal.disposal_type IN ('HANDLE', 'RESOLVE', 'CLEAR', 'CLOSE', 'MISREPORT')
+            GROUP BY disposal.warning_id
+        )
+    </sql>
+
+    <select id="selectIndustryAnalysisSummary" resultType="java.util.Map">
+        <include refid="IndustryWarningCte"/>
+        SELECT warning.industry_code,
+               warning.industry_name,
+               COUNT(*) AS total_count,
+               SUM(CASE WHEN warning.status IN ('PENDING', 'RELEASED') THEN 1 ELSE 0 END) AS pending_count,
+               SUM(CASE WHEN warning.status = 'PROCESSING' THEN 1 ELSE 0 END) AS processing_count,
+               SUM(CASE WHEN warning.status IN ('HANDLED', 'CLOSED', 'RESOLVED') THEN 1 ELSE 0 END) AS completed_count,
+               ROUND(100.0 * SUM(CASE WHEN warning.status IN ('HANDLED', 'CLOSED', 'RESOLVED') THEN 1 ELSE 0 END)
+                     / NULLIF(COUNT(*), 0), 2) AS completion_rate,
+               ROUND(AVG(CASE
+                   WHEN warning.status IN ('HANDLED', 'CLOSED', 'RESOLVED')
+                    AND action.dispose_time &gt;= warning.publish_time
+                   THEN EXTRACT(EPOCH FROM (CAST(action.dispose_time AS TIMESTAMPTZ)
+                                           - CAST(warning.publish_time AS TIMESTAMPTZ))) / 3600
+               END), 2) AS avg_dispose_hours,
+               SUM(CASE
+                   WHEN warning.status IN ('HANDLED', 'CLOSED', 'RESOLVED')
+                    AND action.dispose_time &gt;= warning.publish_time
+                    AND EXTRACT(EPOCH FROM (CAST(action.dispose_time AS TIMESTAMPTZ)
+                                           - CAST(warning.publish_time AS TIMESTAMPTZ))) / 3600 &gt; 24
+                   THEN 1 ELSE 0 END) AS over_24h_count
+        FROM warning_industry warning
+        LEFT JOIN completed_actions action ON action.warning_id = warning.warning_id
+        GROUP BY warning.industry_code, warning.industry_name
+        ORDER BY CASE warning.industry_code
+            WHEN 'gas' THEN 1 WHEN 'water_supply' THEN 2
+            WHEN 'drainage' THEN 3 WHEN 'manhole' THEN 4 ELSE 5 END
+    </select>
+
+    <select id="selectIndustryStatusDistribution" resultType="java.util.Map">
+        <include refid="IndustryWarningCte"/>
+        <!-- CLOSED 与 RESOLVED 是历史上并存的两个“已解除”编码,分析时统一为 CLOSED。 -->
+        SELECT industry_code,
+               industry_name,
+               CASE WHEN status IN ('CLOSED', 'RESOLVED') THEN 'CLOSED' ELSE status END AS status,
+               COUNT(*) AS warning_count
+        FROM warning_industry
+        GROUP BY industry_code,
+                 industry_name,
+                 CASE WHEN status IN ('CLOSED', 'RESOLVED') THEN 'CLOSED' ELSE status END
+        ORDER BY industry_name,
+                 CASE WHEN status IN ('CLOSED', 'RESOLVED') THEN 'CLOSED' ELSE status END
+    </select>
+
+    <select id="selectIndustryEfficiencyComparison" resultType="java.util.Map">
+        <include refid="IndustryWarningCte"/>
+        SELECT warning.industry_code,
+               warning.industry_name,
+               COUNT(*) AS completed_count,
+               ROUND(AVG(EXTRACT(EPOCH FROM (CAST(action.dispose_time AS TIMESTAMPTZ)
+                                            - CAST(warning.publish_time AS TIMESTAMPTZ))) / 3600), 2) AS avg_dispose_hours,
+               ROUND(MAX(EXTRACT(EPOCH FROM (CAST(action.dispose_time AS TIMESTAMPTZ)
+                                            - CAST(warning.publish_time AS TIMESTAMPTZ))) / 3600), 2) AS max_dispose_hours,
+               ROUND(MIN(EXTRACT(EPOCH FROM (CAST(action.dispose_time AS TIMESTAMPTZ)
+                                            - CAST(warning.publish_time AS TIMESTAMPTZ))) / 3600), 2) AS min_dispose_hours,
+               ROUND(100.0 * SUM(CASE WHEN EXTRACT(EPOCH FROM (CAST(action.dispose_time AS TIMESTAMPTZ)
+                                            - CAST(warning.publish_time AS TIMESTAMPTZ))) / 3600 &lt;= 24
+                                      THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0), 2) AS within_24h_rate
+        FROM warning_industry warning
+        INNER JOIN completed_actions action ON action.warning_id = warning.warning_id
+        WHERE warning.status IN ('HANDLED', 'CLOSED', 'RESOLVED')
+          AND action.dispose_time &gt;= warning.publish_time
+        GROUP BY warning.industry_code, warning.industry_name
+        ORDER BY avg_dispose_hours DESC
+    </select>
+
+    <select id="selectIndustryAnalysisList" resultType="java.util.Map">
+        <include refid="IndustryWarningCte"/>
+        SELECT warning.warning_id,
+               warning.warning_no,
+               warning.warning_name,
+               warning.device_code,
+               warning.device_name,
+               warning.industry_code,
+               warning.industry_name,
+               warning.warning_level,
+               warning.publish_time,
+               warning.status,
+               action.dispose_time,
+               CASE WHEN action.dispose_time &gt;= warning.publish_time
+                    THEN ROUND(EXTRACT(EPOCH FROM (CAST(action.dispose_time AS TIMESTAMPTZ)
+                                                 - CAST(warning.publish_time AS TIMESTAMPTZ))) / 3600, 2)
+                    ELSE NULL END AS dispose_hours
+        FROM warning_industry warning
+        LEFT JOIN completed_actions action ON action.warning_id = warning.warning_id
+        <where>
+            <if test="status != null and status != ''">
+                <choose>
+                    <!-- 页面只提供一个“已解除”筛选项,但兼容两种历史状态值。 -->
+                    <when test="status == 'CLOSED' or status == 'RESOLVED'">
+                        warning.status IN ('CLOSED', 'RESOLVED')
+                    </when>
+                    <otherwise>
+                        warning.status = #{status}
+                    </otherwise>
+                </choose>
+            </if>
+        </where>
+        ORDER BY dispose_hours
+        <if test="sortType != null and sortType == 'desc'">DESC</if>
+        <if test="sortType == null or sortType == 'asc'">ASC</if>
+        NULLS LAST, warning.publish_time DESC
+    </select>
+
 </mapper>