1
0

3 Коммиты e6299cebe2 ... 566390832f

Автор SHA1 Сообщение Дата
  Kazerin 566390832f fix(water): classify facility statistics statuses (#7) 3 дней назад
  Kazerin 0a5cc990fe feat(water): stabilize pipe network statistics (#7) 3 дней назад
  Kazerin f11e3bbfda feat(water): add facility excel import 4 дней назад
13 измененных файлов с 803 добавлено и 22 удалено
  1. 24 0
      pipe-network-service/sql/water_supply_facility_import.sql
  2. 2 2
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsController.java
  3. 66 1
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/WaterFacilityExportController.java
  4. 14 0
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsControllerTerminologyTest.java
  5. 25 0
      pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/WaterFacilityExportControllerContractTest.java
  6. 2 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/dto/FacilityStatisticItemDTO.java
  7. 39 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/impl/PipeNetworkStatisticsExportServiceImpl.java
  8. 30 9
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/impl/PipeNetworkStatisticsServiceImpl.java
  9. 199 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/importing/WaterFacilityImportService.java
  10. 22 10
      pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsMapper.xml
  11. 86 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsExportServiceContractTest.java
  12. 72 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsServiceContractTest.java
  13. 222 0
      pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/WaterFacilityImportServiceTest.java

+ 24 - 0
pipe-network-service/sql/water_supply_facility_import.sql

@@ -0,0 +1,24 @@
+-- 供水设施 Excel 批量导入按钮权限(KingbaseES / PostgreSQL 兼容)
+-- 可重复执行;只补充导入按钮权限,不修改页面菜单和既有权限。
+
+DO $$
+BEGIN
+    INSERT INTO app_user.sys_menu (
+        menu_name, parent_id, order_num, path, component, is_frame, is_cache,
+        menu_type, visible, status, perms, icon, create_by, create_time, remark
+    )
+    SELECT
+        v.menu_name, page_menu.menu_id, 30, '', '', '1', '0',
+        'F', '0', '0', v.perms, '#', 'admin', now(), '供水设施 Excel 批量导入'
+    FROM (VALUES
+        ('水源地导入', 'waterSupply/facility/source', 'waterSupply:waterSource:import'),
+        ('水厂导入', 'waterSupply/facility/plant', 'waterSupply:waterPlant:import'),
+        ('泵站导入', 'waterSupply/facility/pumpStation', 'waterSupply:waterPumpStation:import'),
+        ('管网导入', 'waterSupply/facility/pipe', 'waterSupply:waterPipe:import'),
+        ('用水户导入', 'waterSupply/facility/user', 'waterSupply:waterUser:import')
+    ) AS v(menu_name, parent_component, perms)
+    JOIN app_user.sys_menu page_menu ON page_menu.component = v.parent_component
+    WHERE NOT EXISTS (
+        SELECT 1 FROM app_user.sys_menu existing WHERE existing.perms = v.perms
+    );
+END $$;

+ 2 - 2
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsController.java

@@ -37,7 +37,7 @@ public class PipeNetworkStatisticsController {
             notes = "请求参数为可选查询条件。响应 data 包含 facilityOverview、pipeNetwork、waterPlant、pumpStation、waterSource、waterUser 六个区块;分组项结构为 {name, value, unit}。")
     @PreAuthorize("@ss.hasPermi('waterSupply:pipeNetwork:statistics')")
     public AjaxResult dashboard(
-            @ApiParam(value = "设施状态:0正常、1/2异常;不传查询全部", example = "0")
+            @ApiParam(value = "设施状态:0正常;1/2按设施类型分别表示异常、停产、故障停运、停用、欠费、检修中;不传查询全部", example = "0")
             @RequestParam(required = false) String status,
             @ApiParam(value = "设施类型,逗号分隔:source、plant、pumpStation、pipe、user;不传查询全部", example = "plant,pipe")
             @RequestParam(required = false) String facilityTypes) {
@@ -55,7 +55,7 @@ public class PipeNetworkStatisticsController {
     @Log(title = "管网数据统计分析", businessType = BusinessType.EXPORT)
     @PreAuthorize("@ss.hasPermi('waterSupply:pipeNetwork:statistics:export')")
     public void export(HttpServletResponse response,
-                       @ApiParam(value = "设施状态:0正常、1/2异常;不传查询全部", example = "0")
+                       @ApiParam(value = "设施状态:0正常;1/2按设施类型分别表示异常、停产、故障停运、停用、欠费、检修中;不传查询全部", example = "0")
                        @RequestParam(required = false) String status,
                        @ApiParam(value = "设施类型,逗号分隔:source、plant、pumpStation、pipe、user;不传查询全部", example = "source,user")
                        @RequestParam(required = false) String facilityTypes) throws IOException {

+ 66 - 1
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/WaterFacilityExportController.java

@@ -12,16 +12,21 @@ import com.zksy.WaterSupply.WaterSourceInfo.service.IWaterSourceInfoService;
 import com.zksy.WaterSupply.WaterUserInfo.domain.WaterUserInfo;
 import com.zksy.WaterSupply.WaterUserInfo.service.IWaterUserInfoService;
 import com.zksy.WaterSupply.export.*;
+import com.zksy.WaterSupply.importing.WaterFacilityImportService;
 import com.zksy.common.annotation.Log;
 import com.zksy.common.annotation.Excel;
+import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.enums.BusinessType;
 import com.zksy.common.utils.poi.ExcelUtil;
+import com.zksy.common.utils.SecurityUtils;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.format.annotation.DateTimeFormat;
 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 org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
@@ -36,6 +41,11 @@ public class WaterFacilityExportController {
     @Resource private IWaterPumpStationService pumpService;
     @Resource private IWaterPipeInfoService pipeService;
     @Resource private IWaterUserInfoService userService;
+    @Resource private WaterFacilityImportService importService;
+
+    private String operator() {
+        return SecurityUtils.getUsername();
+    }
 
     @GetMapping("/sourceInfo/export")
     @Log(title = "导出水源地信息", businessType = BusinessType.EXPORT)
@@ -76,4 +86,59 @@ public class WaterFacilityExportController {
         var page = userService.queryPage(new Page<WaterUserInfo>(1, Integer.MAX_VALUE), userCode, userName, status, userType, startTime, endTime);
         new ExcelUtil<>(WaterUserExportDTO.class).exportExcel(response, page.getRecords().stream().map(WaterUserExportDTO::from).collect(Collectors.toList()), "用水户信息");
     }
-}
+
+    @PostMapping("/sourceInfo/importData")
+    @Log(title = "导入水源地信息", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterSource:import')")
+    public AjaxResult sourceImportData(@RequestParam("file") MultipartFile file, @RequestParam(defaultValue = "false") boolean updateSupport) throws Exception {
+        return AjaxResult.success(importService.importSources(new ExcelUtil<WaterSourceExportDTO>(WaterSourceExportDTO.class).importExcel(file.getInputStream()), updateSupport, operator()));
+    }
+
+    @PostMapping("/sourceInfo/importTemplate")
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterSource:import')")
+    public void sourceImportTemplate(HttpServletResponse response) { new ExcelUtil<WaterSourceExportDTO>(WaterSourceExportDTO.class).importTemplateExcel(response, "水源地信息"); }
+
+    @PostMapping("/plantInfo/importData")
+    @Log(title = "导入水厂信息", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterPlant:import')")
+    public AjaxResult plantImportData(@RequestParam("file") MultipartFile file, @RequestParam(defaultValue = "false") boolean updateSupport) throws Exception {
+        return AjaxResult.success(importService.importPlants(new ExcelUtil<WaterPlantExportDTO>(WaterPlantExportDTO.class).importExcel(file.getInputStream()), updateSupport, operator()));
+    }
+
+    @PostMapping("/plantInfo/importTemplate")
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterPlant:import')")
+    public void plantImportTemplate(HttpServletResponse response) { new ExcelUtil<WaterPlantExportDTO>(WaterPlantExportDTO.class).importTemplateExcel(response, "水厂信息"); }
+
+    @PostMapping("/pumpStation/importData")
+    @Log(title = "导入泵站信息", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterPumpStation:import')")
+    public AjaxResult pumpImportData(@RequestParam("file") MultipartFile file, @RequestParam(defaultValue = "false") boolean updateSupport) throws Exception {
+        return AjaxResult.success(importService.importPumps(new ExcelUtil<WaterPumpStationExportDTO>(WaterPumpStationExportDTO.class).importExcel(file.getInputStream()), updateSupport, operator()));
+    }
+
+    @PostMapping("/pumpStation/importTemplate")
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterPumpStation:import')")
+    public void pumpImportTemplate(HttpServletResponse response) { new ExcelUtil<WaterPumpStationExportDTO>(WaterPumpStationExportDTO.class).importTemplateExcel(response, "泵站信息"); }
+
+    @PostMapping("/pipe/importData")
+    @Log(title = "导入管网信息", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterPipe:import')")
+    public AjaxResult pipeImportData(@RequestParam("file") MultipartFile file, @RequestParam(defaultValue = "false") boolean updateSupport) throws Exception {
+        return AjaxResult.success(importService.importPipes(new ExcelUtil<WaterPipeExportDTO>(WaterPipeExportDTO.class).importExcel(file.getInputStream()), updateSupport, operator()));
+    }
+
+    @PostMapping("/pipe/importTemplate")
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterPipe:import')")
+    public void pipeImportTemplate(HttpServletResponse response) { new ExcelUtil<WaterPipeExportDTO>(WaterPipeExportDTO.class).importTemplateExcel(response, "管网信息"); }
+
+    @PostMapping("/userInfo/importData")
+    @Log(title = "导入用水户信息", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterUser:import')")
+    public AjaxResult userImportData(@RequestParam("file") MultipartFile file, @RequestParam(defaultValue = "false") boolean updateSupport) throws Exception {
+        return AjaxResult.success(importService.importUsers(new ExcelUtil<WaterUserExportDTO>(WaterUserExportDTO.class).importExcel(file.getInputStream()), updateSupport, operator()));
+    }
+
+    @PostMapping("/userInfo/importTemplate")
+    @PreAuthorize("@ss.hasPermi('waterSupply:waterUser:import')")
+    public void userImportTemplate(HttpServletResponse response) { new ExcelUtil<WaterUserExportDTO>(WaterUserExportDTO.class).importTemplateExcel(response, "用水户信息"); }
+}

+ 14 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsControllerTerminologyTest.java

@@ -1,6 +1,7 @@
 package com.zksy.web.controller.WaterSupply.PipeNetworkStatistics;
 
 import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
 import org.junit.jupiter.api.Test;
 
 import javax.servlet.http.HttpServletResponse;
@@ -20,4 +21,17 @@ class PipeNetworkStatisticsControllerTerminologyTest {
         assertTrue(operation.notes().contains("用水户统计"));
         assertFalse(operation.notes().contains("大用水户统计"));
     }
+
+    @Test
+    void statusDescriptionsUseFacilitySpecificTerminology() throws Exception {
+        for (Method method : PipeNetworkStatisticsController.class.getMethods()) {
+            for (java.lang.reflect.Parameter parameter : method.getParameters()) {
+                ApiParam annotation = parameter.getAnnotation(ApiParam.class);
+                if (annotation != null && annotation.value().startsWith("设施状态")) {
+                    assertFalse(annotation.value().contains("1/2异常"));
+                    assertTrue(annotation.value().contains("按设施类型"));
+                }
+            }
+        }
+    }
 }

+ 25 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/WaterSupply/WaterFacilityExportControllerContractTest.java

@@ -2,6 +2,7 @@ package com.zksy.web.controller.WaterSupply;
 
 import org.junit.jupiter.api.Test;
 import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.PostMapping;
 
 import java.lang.reflect.Method;
 import java.time.LocalDateTime;
@@ -12,6 +13,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class WaterFacilityExportControllerContractTest {
 
+    @Test
+    void allFacilityControllersExposeExcelImportAndTemplateEndpoints() {
+        assertPostMapping(WaterFacilityExportController.class, "sourceImportData", "/sourceInfo/importData");
+        assertPostMapping(WaterFacilityExportController.class, "sourceImportTemplate", "/sourceInfo/importTemplate");
+        assertPostMapping(WaterFacilityExportController.class, "plantImportData", "/plantInfo/importData");
+        assertPostMapping(WaterFacilityExportController.class, "plantImportTemplate", "/plantInfo/importTemplate");
+        assertPostMapping(WaterFacilityExportController.class, "pumpImportData", "/pumpStation/importData");
+        assertPostMapping(WaterFacilityExportController.class, "pumpImportTemplate", "/pumpStation/importTemplate");
+        assertPostMapping(WaterFacilityExportController.class, "pipeImportData", "/pipe/importData");
+        assertPostMapping(WaterFacilityExportController.class, "pipeImportTemplate", "/pipe/importTemplate");
+        assertPostMapping(WaterFacilityExportController.class, "userImportData", "/userInfo/importData");
+        assertPostMapping(WaterFacilityExportController.class, "userImportTemplate", "/userInfo/importTemplate");
+    }
+
     @Test
     void allFacilityExportsUseTheFrontendDateTimeContract() {
         for (String methodName : new String[]{"sourceExport", "plantExport", "pumpExport", "pipeExport", "userExport"}) {
@@ -29,4 +44,14 @@ class WaterFacilityExportControllerContractTest {
                     });
         }
     }
+
+    private static void assertPostMapping(Class<?> controller, String methodName, String path) {
+        Method method = Arrays.stream(controller.getDeclaredMethods())
+                .filter(candidate -> candidate.getName().equals(methodName))
+                .findFirst()
+                .orElseThrow(() -> new AssertionError(controller.getSimpleName() + " must declare " + methodName));
+        PostMapping mapping = method.getAnnotation(PostMapping.class);
+        assertTrue(mapping != null, methodName + " must use @PostMapping");
+        assertTrue(Arrays.asList(mapping.value()).contains(path), methodName + " must map " + path);
+    }
 }

+ 2 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/dto/FacilityStatisticItemDTO.java

@@ -9,4 +9,6 @@ public class FacilityStatisticItemDTO {
     private Long totalCount;
     private Long normalCount;
     private Long abnormalCount;
+    private Long warningCount;
+    private Long unknownCount;
 }

+ 39 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/impl/PipeNetworkStatisticsExportServiceImpl.java

@@ -1,5 +1,6 @@
 package com.zksy.WaterSupply.PipeNetworkStatistics.service.impl;
 
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO;
 import com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO;
 import com.zksy.WaterSupply.PipeNetworkStatistics.dto.PipeNetworkStatisticsDashboardDTO;
 import com.zksy.WaterSupply.PipeNetworkStatistics.service.PipeNetworkStatisticsExportService;
@@ -42,10 +43,48 @@ public class PipeNetworkStatisticsExportServiceImpl implements PipeNetworkStatis
                 row.createCell(1).setCellValue(item.getFacilityTypeName());
                 row.createCell(2).setCellValue(item.getTotalCount() == null ? 0 : item.getTotalCount());
                 row.createCell(3).setCellValue("项");
+
+                addOverviewRow(sheet, rowIndex++, item, "正常", item.getNormalCount());
+                addOverviewRow(sheet, rowIndex++, item, abnormalStatusLabel(item.getFacilityType()), item.getAbnormalCount());
+                if (hasWarningStatus(item.getFacilityType())) {
+                    addOverviewRow(sheet, rowIndex++, item, warningStatusLabel(item.getFacilityType()), item.getWarningCount());
+                }
+                addOverviewRow(sheet, rowIndex++, item, "未知", item.getUnknownCount());
             }
         }
     }
 
+    private boolean hasWarningStatus(String facilityType) {
+        return "pumpStation".equals(facilityType) || "user".equals(facilityType);
+    }
+
+    private String abnormalStatusLabel(String facilityType) {
+        switch (facilityType) {
+            case "plant":
+                return "停产";
+            case "pumpStation":
+                return "故障停运";
+            case "pipe":
+                return "停用";
+            case "user":
+                return "停用";
+            default:
+                return "异常";
+        }
+    }
+
+    private String warningStatusLabel(String facilityType) {
+        return "pumpStation".equals(facilityType) ? "检修中" : "欠费";
+    }
+
+    private void addOverviewRow(Sheet sheet, int rowIndex, FacilityStatisticItemDTO item, String label, Long count) {
+        Row row = sheet.createRow(rowIndex);
+        row.createCell(0).setCellValue("状态统计");
+        row.createCell(1).setCellValue(item.getFacilityTypeName() + "-" + label);
+        row.createCell(2).setCellValue(count == null ? 0 : count);
+        row.createCell(3).setCellValue("项");
+    }
+
     private void writeMapSheet(Sheet sheet, Map<String, Object> values) {
         header(sheet);
         if (values == null) return;

+ 30 - 9
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/PipeNetworkStatistics/service/impl/PipeNetworkStatisticsServiceImpl.java

@@ -14,6 +14,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.ArrayList;
 import java.util.stream.Collectors;
 
 @Service
@@ -29,10 +30,19 @@ public class PipeNetworkStatisticsServiceImpl implements PipeNetworkStatisticsSe
         Set<String> types = facilityTypes == null || facilityTypes.isEmpty() ? SUPPORTED_TYPES : facilityTypes;
         PipeNetworkStatisticsDashboardDTO dashboard = new PipeNetworkStatisticsDashboardDTO();
         List<com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO> overview = statisticsMapper.selectFacilityOverview(status);
-        dashboard.setFacilityOverview((overview == null ? Collections.<com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO>emptyList() : overview)
-                .stream().filter(item -> types.contains(item.getFacilityType())).collect(Collectors.toList()));
+        List<com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO> filtered = (overview == null ? Collections.<com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO>emptyList() : overview)
+                .stream().filter(item -> types.contains(item.getFacilityType())).collect(Collectors.toList());
+        if (filtered.isEmpty()) {
+            filtered = new ArrayList<>();
+            addZeroOverview(filtered, types, "source", "水源地");
+            addZeroOverview(filtered, types, "plant", "水厂");
+            addZeroOverview(filtered, types, "pumpStation", "泵站");
+            addZeroOverview(filtered, types, "pipe", "供水管网");
+            addZeroOverview(filtered, types, "user", "用水户");
+        }
+        dashboard.setFacilityOverview(filtered);
         if (types.contains("pipe")) {
-            Map<String, Object> result = zeroSafe(statisticsMapper.selectPipeSummary(status));
+            Map<String, Object> result = zeroSafe(statisticsMapper.selectPipeSummary(status), "totalCount");
             result.put("materialDistribution", list(statisticsMapper.selectPipeMaterialDistribution(status)));
             result.put("diameterDistribution", list(statisticsMapper.selectPipeDiameterDistribution(status)));
             result.put("layingYearDistribution", list(statisticsMapper.selectPipeLayingYearDistribution(status)));
@@ -41,19 +51,19 @@ public class PipeNetworkStatisticsServiceImpl implements PipeNetworkStatisticsSe
             dashboard.setPipeNetwork(result);
         }
         if (types.contains("plant")) {
-            Map<String, Object> result = zeroSafe(statisticsMapper.selectWaterPlantSummary(status));
+            Map<String, Object> result = zeroSafe(statisticsMapper.selectWaterPlantSummary(status), "totalCount");
             result.put("capacityUtilization", utilization(result));
             result.put("statusDistribution", list(statisticsMapper.selectStatusDistribution("plant", status)));
             result.put("waterSourceTypeDistribution", list(statisticsMapper.selectWaterPlantSourceTypeDistribution(status)));
             dashboard.setWaterPlant(result);
         }
         if (types.contains("pumpStation")) {
-            Map<String, Object> result = zeroSafe(statisticsMapper.selectPumpStationSummary(status));
+            Map<String, Object> result = zeroSafe(statisticsMapper.selectPumpStationSummary(status), "totalCount");
             result.put("statusDistribution", list(statisticsMapper.selectStatusDistribution("pumpStation", status)));
             dashboard.setPumpStation(result);
         }
         if (types.contains("source")) {
-            Map<String, Object> result = zeroSafe(statisticsMapper.selectWaterSourceSummary(status));
+            Map<String, Object> result = zeroSafe(statisticsMapper.selectWaterSourceSummary(status), "totalCount");
             result.put("statusDistribution", list(statisticsMapper.selectStatusDistribution("source", status)));
             result.put("sourceTypeDistribution", list(statisticsMapper.selectWaterSourceTypeDistribution(status)));
             result.put("qualityDistribution", list(statisticsMapper.selectWaterSourceQualityDistribution(status)));
@@ -61,7 +71,7 @@ public class PipeNetworkStatisticsServiceImpl implements PipeNetworkStatisticsSe
             dashboard.setWaterSource(result);
         }
         if (types.contains("user")) {
-            Map<String, Object> result = zeroSafe(statisticsMapper.selectWaterUserSummary(status));
+            Map<String, Object> result = zeroSafe(statisticsMapper.selectWaterUserSummary(status), "totalCount");
             result.put("statusDistribution", list(statisticsMapper.selectStatusDistribution("user", status)));
             result.put("userTypeDistribution", list(statisticsMapper.selectWaterUserTypeDistribution(status)));
             result.put("consumptionDistribution", list(statisticsMapper.selectWaterUserConsumptionDistribution(status)));
@@ -76,8 +86,19 @@ public class PipeNetworkStatisticsServiceImpl implements PipeNetworkStatisticsSe
         }
     }
 
-    private Map<String, Object> zeroSafe(Map<String, Object> value) {
-        return value == null ? new LinkedHashMap<>() : new LinkedHashMap<>(value);
+    private Map<String, Object> zeroSafe(Map<String, Object> value, String... zeroKeys) {
+        Map<String, Object> result = value == null ? new LinkedHashMap<>() : new LinkedHashMap<>(value);
+        for (String key : zeroKeys) result.putIfAbsent(key, 0L);
+        return result;
+    }
+
+    private void addZeroOverview(List<com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO> target,
+                                 Set<String> types, String type, String label) {
+        if (!types.contains(type)) return;
+        com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO item = new com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO();
+        item.setFacilityType(type); item.setFacilityTypeName(label); item.setTotalCount(0L); item.setNormalCount(0L); item.setAbnormalCount(0L);
+        item.setWarningCount(0L); item.setUnknownCount(0L);
+        target.add(item);
     }
 
     private List<MetricItemDTO> list(List<MetricItemDTO> value) {

+ 199 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/importing/WaterFacilityImportService.java

@@ -0,0 +1,199 @@
+package com.zksy.WaterSupply.importing;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zksy.WaterSupply.WaterPipeInfo.domain.WaterPipeInfo;
+import com.zksy.WaterSupply.WaterPipeInfo.service.IWaterPipeInfoService;
+import com.zksy.WaterSupply.WaterPlantInfo.domain.WaterPlantInfo;
+import com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService;
+import com.zksy.WaterSupply.WaterPumpStation.domain.WaterPumpStation;
+import com.zksy.WaterSupply.WaterPumpStation.service.IWaterPumpStationService;
+import com.zksy.WaterSupply.WaterSourceInfo.domain.WaterSourceInfo;
+import com.zksy.WaterSupply.WaterSourceInfo.service.IWaterSourceInfoService;
+import com.zksy.WaterSupply.WaterUserInfo.domain.WaterUserInfo;
+import com.zksy.WaterSupply.WaterUserInfo.service.IWaterUserInfoService;
+import com.zksy.WaterSupply.export.WaterPipeExportDTO;
+import com.zksy.WaterSupply.export.WaterPlantExportDTO;
+import com.zksy.WaterSupply.export.WaterPumpStationExportDTO;
+import com.zksy.WaterSupply.export.WaterSourceExportDTO;
+import com.zksy.WaterSupply.export.WaterUserExportDTO;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/** 五类供水设施 Excel 导入的统一业务适配器。 */
+@Transactional(rollbackFor = Exception.class)
+@Service
+public class WaterFacilityImportService {
+    private final IWaterSourceInfoService sourceService;
+    private final IWaterPlantInfoService plantService;
+    private final IWaterPumpStationService pumpService;
+    private final IWaterPipeInfoService pipeService;
+    private final IWaterUserInfoService userService;
+
+    public WaterFacilityImportService(IWaterSourceInfoService sourceService,
+                                      IWaterPlantInfoService plantService,
+                                      IWaterPumpStationService pumpService,
+                                      IWaterPipeInfoService pipeService,
+                                      IWaterUserInfoService userService) {
+        this.sourceService = sourceService;
+        this.plantService = plantService;
+        this.pumpService = pumpService;
+        this.pipeService = pipeService;
+        this.userService = userService;
+    }
+
+    public String importSources(List<WaterSourceExportDTO> rows, boolean updateSupport, String operator) {
+        return importRows(rows, updateSupport, operator, new ImportPlan<WaterSourceInfo, WaterSourceExportDTO>(
+                WaterSourceExportDTO::getSourceId,
+                code -> sourceService.getOne(Wrappers.<WaterSourceInfo>lambdaQuery().eq(WaterSourceInfo::getSourceId, code), false),
+                WaterSourceInfo::new,
+                (row, entity) -> BeanUtils.copyProperties(row, entity),
+                sourceService,
+                this::prepareSource), "水源地数据");
+    }
+
+    public String importPlants(List<WaterPlantExportDTO> rows, boolean updateSupport, String operator) {
+        return importRows(rows, updateSupport, operator, new ImportPlan<WaterPlantInfo, WaterPlantExportDTO>(
+                WaterPlantExportDTO::getPlantCode,
+                code -> plantService.getOne(Wrappers.<WaterPlantInfo>lambdaQuery().eq(WaterPlantInfo::getPlantCode, code), false),
+                WaterPlantInfo::new,
+                (row, entity) -> BeanUtils.copyProperties(row, entity),
+                plantService,
+                this::preparePlant), "水厂数据");
+    }
+
+    public String importPumps(List<WaterPumpStationExportDTO> rows, boolean updateSupport, String operator) {
+        return importRows(rows, updateSupport, operator, new ImportPlan<WaterPumpStation, WaterPumpStationExportDTO>(
+                WaterPumpStationExportDTO::getStationCode,
+                code -> pumpService.getOne(Wrappers.<WaterPumpStation>lambdaQuery().eq(WaterPumpStation::getStationCode, code), false),
+                WaterPumpStation::new,
+                (row, entity) -> BeanUtils.copyProperties(row, entity),
+                pumpService,
+                this::preparePump), "泵站数据");
+    }
+
+    public String importPipes(List<WaterPipeExportDTO> rows, boolean updateSupport, String operator) {
+        return importRows(rows, updateSupport, operator, new ImportPlan<WaterPipeInfo, WaterPipeExportDTO>(
+                WaterPipeExportDTO::getPipeCode,
+                code -> pipeService.getOne(Wrappers.<WaterPipeInfo>lambdaQuery().eq(WaterPipeInfo::getPipeCode, code), false),
+                WaterPipeInfo::new,
+                (row, entity) -> BeanUtils.copyProperties(row, entity),
+                pipeService,
+                this::preparePipe), "管网数据");
+    }
+
+    public String importUsers(List<WaterUserExportDTO> rows, boolean updateSupport, String operator) {
+        return importRows(rows, updateSupport, operator, new ImportPlan<WaterUserInfo, WaterUserExportDTO>(
+                WaterUserExportDTO::getUserCode,
+                code -> userService.getOne(Wrappers.<WaterUserInfo>lambdaQuery().eq(WaterUserInfo::getUserCode, code), false),
+                WaterUserInfo::new,
+                (row, entity) -> BeanUtils.copyProperties(row, entity),
+                userService,
+                this::prepareUser), "用水户数据");
+    }
+
+    private <T, R> String importRows(List<R> rows, boolean updateSupport, String operator,
+                                     ImportPlan<T, R> plan, String noun) {
+        int count = 0;
+        LocalDateTime now = LocalDateTime.now();
+        for (R row : rows) {
+            String code = plan.code.apply(row);
+            if (code == null || code.trim().isEmpty()) continue;
+            T entity = plan.existing.apply(code);
+            boolean update = entity != null;
+            if (update && !updateSupport) continue;
+            if (entity == null) entity = plan.factory.get();
+            plan.copier.accept(row, entity);
+            plan.preparer.prepare(entity, update, operator, now);
+            boolean persisted = update ? plan.service.updateById(entity) : plan.service.save(entity);
+            if (!persisted) throw new IllegalStateException("供水设施数据保存失败");
+            count++;
+        }
+        return "成功导入 " + count + " 条" + noun;
+    }
+
+    private void prepareSource(WaterSourceInfo value, boolean update, String operator, LocalDateTime now) {
+        if (update) { value.setUpdateBy(operator); value.setUpdateTime(now); return; }
+        initialize(value, operator, now);
+    }
+
+    private void preparePlant(WaterPlantInfo value, boolean update, String operator, LocalDateTime now) {
+        if (update) { value.setUpdateBy(operator); value.setUpdateTime(now); return; }
+        initialize(value, operator, now);
+    }
+
+    private void preparePump(WaterPumpStation value, boolean update, String operator, LocalDateTime now) {
+        if (update) { value.setUpdateBy(operator); value.setUpdateTime(now); return; }
+        initialize(value, operator, now);
+    }
+
+    private void preparePipe(WaterPipeInfo value, boolean update, String operator, LocalDateTime now) {
+        if (update) { value.setUpdateBy(operator); value.setUpdateTime(now); return; }
+        initialize(value, operator, now);
+    }
+
+    private void prepareUser(WaterUserInfo value, boolean update, String operator, LocalDateTime now) {
+        if (update) { value.setUpdateBy(operator); value.setUpdateTime(now); return; }
+        initialize(value, operator, now);
+    }
+
+    private void initialize(WaterSourceInfo value, String operator, LocalDateTime now) {
+        value.setDelFlag("0"); value.setCreateBy(operator); value.setCreateTime(now);
+        if (value.getStatus() == null) value.setStatus("0");
+    }
+
+    private void initialize(WaterPlantInfo value, String operator, LocalDateTime now) {
+        value.setDelFlag("0"); value.setCreateBy(operator); value.setCreateTime(now);
+        if (value.getStatus() == null) value.setStatus("0");
+    }
+
+    private void initialize(WaterPumpStation value, String operator, LocalDateTime now) {
+        value.setDelFlag("0"); value.setCreateBy(operator); value.setCreateTime(now);
+        if (value.getStatus() == null) value.setStatus("0");
+    }
+
+    private void initialize(WaterPipeInfo value, String operator, LocalDateTime now) {
+        value.setDelFlag("0"); value.setCreateBy(operator); value.setCreateTime(now);
+        if (value.getStatus() == null) value.setStatus("0");
+    }
+
+    private void initialize(WaterUserInfo value, String operator, LocalDateTime now) {
+        value.setDelFlag("0"); value.setCreateBy(operator); value.setCreateTime(now);
+        if (value.getStatus() == null) value.setStatus("0");
+    }
+
+    @FunctionalInterface
+    private interface AuditPreparer<T> {
+        void prepare(T entity, boolean update, String operator, LocalDateTime now);
+    }
+
+    private static final class ImportPlan<T, R> {
+        private final Function<R, String> code;
+        private final Function<String, T> existing;
+        private final Supplier<T> factory;
+        private final BiConsumer<R, T> copier;
+        private final IService<T> service;
+        private final AuditPreparer<T> preparer;
+
+        private ImportPlan(Function<R, String> code,
+                           Function<String, T> existing,
+                           Supplier<T> factory,
+                           BiConsumer<R, T> copier,
+                           IService<T> service,
+                           AuditPreparer<T> preparer) {
+            this.code = code;
+            this.existing = existing;
+            this.factory = factory;
+            this.copier = copier;
+            this.service = service;
+            this.preparer = preparer;
+        }
+    }
+}

+ 22 - 10
pipe-network-service/zksy-system/src/main/resources/mapper/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsMapper.xml

@@ -4,23 +4,35 @@
     <select id="selectFacilityOverview" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO">
         SELECT 'source' facility_type, '水源地' facility_type_name, COUNT(*) total_count,
                COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0) normal_count,
-               COALESCE(SUM(CASE WHEN status IS NOT NULL AND status &lt;&gt; '0' THEN 1 ELSE 0 END), 0) abnormal_count
+               COALESCE(SUM(CASE WHEN status = '1' THEN 1 ELSE 0 END), 0) abnormal_count,
+               0 warning_count,
+               COALESCE(SUM(CASE WHEN status IS NULL OR status NOT IN ('0', '1') THEN 1 ELSE 0 END), 0) unknown_count
         FROM app_user.water_source_info WHERE del_flag = '0'
         <if test="status != null and status != ''">AND status = #{status}</if>
         UNION ALL
-        SELECT 'plant', '水厂', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status IS NOT NULL AND status &lt;&gt; '0' THEN 1 ELSE 0 END), 0)
+        SELECT 'plant', '水厂', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0),
+               COALESCE(SUM(CASE WHEN status = '1' THEN 1 ELSE 0 END), 0), 0,
+               COALESCE(SUM(CASE WHEN status IS NULL OR status NOT IN ('0', '1') THEN 1 ELSE 0 END), 0)
         FROM app_user.water_plant_info WHERE del_flag = '0'
         <if test="status != null and status != ''">AND status = #{status}</if>
         UNION ALL
-        SELECT 'pumpStation', '泵站', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status IS NOT NULL AND status &lt;&gt; '0' THEN 1 ELSE 0 END), 0)
+        SELECT 'pumpStation', '泵站', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0),
+               COALESCE(SUM(CASE WHEN status = '1' THEN 1 ELSE 0 END), 0),
+               COALESCE(SUM(CASE WHEN status = '2' THEN 1 ELSE 0 END), 0),
+               COALESCE(SUM(CASE WHEN status IS NULL OR status NOT IN ('0', '1', '2') THEN 1 ELSE 0 END), 0)
         FROM app_user.water_pump_station WHERE del_flag = '0'
         <if test="status != null and status != ''">AND status = #{status}</if>
         UNION ALL
-        SELECT 'pipe', '供水管网', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status IS NOT NULL AND status &lt;&gt; '0' THEN 1 ELSE 0 END), 0)
+        SELECT 'pipe', '供水管网', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0),
+               COALESCE(SUM(CASE WHEN status = '1' THEN 1 ELSE 0 END), 0), 0,
+               COALESCE(SUM(CASE WHEN status IS NULL OR status NOT IN ('0', '1') THEN 1 ELSE 0 END), 0)
         FROM app_user.water_pipe_info WHERE del_flag = '0'
         <if test="status != null and status != ''">AND status = #{status}</if>
         UNION ALL
-        SELECT 'user', '用水户', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN status IS NOT NULL AND status &lt;&gt; '0' THEN 1 ELSE 0 END), 0)
+        SELECT 'user', '用水户', COUNT(*), COALESCE(SUM(CASE WHEN status = '0' THEN 1 ELSE 0 END), 0),
+               COALESCE(SUM(CASE WHEN status = '2' THEN 1 ELSE 0 END), 0),
+               COALESCE(SUM(CASE WHEN status = '1' THEN 1 ELSE 0 END), 0),
+               COALESCE(SUM(CASE WHEN status IS NULL OR status NOT IN ('0', '1', '2') THEN 1 ELSE 0 END), 0)
         FROM app_user.water_user_info WHERE del_flag = '0'
         <if test="status != null and status != ''">AND status = #{status}</if>
     </select>
@@ -64,13 +76,13 @@
     </select>
     <select id="selectPipeLayingYearDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
         SELECT CASE WHEN laying_year IS NULL THEN CAST('未填报' AS VARCHAR)
-          ELSE CONCAT(CAST((laying_year / 10) * 10 AS VARCHAR), '-',
-                      CAST((laying_year / 10) * 10 + 9 AS VARCHAR)) END name,
+          ELSE CONCAT(CAST(FLOOR(laying_year / 10) * 10 AS INTEGER), '-',
+                      CAST(FLOOR(laying_year / 10) * 10 + 9 AS INTEGER)) END name,
           COUNT(*) "value", '条' unit FROM app_user.water_pipe_info WHERE del_flag = '0'
         <if test="status != null and status != ''">AND status = #{status}</if>
         GROUP BY CASE WHEN laying_year IS NULL THEN CAST('未填报' AS VARCHAR)
-          ELSE CONCAT(CAST((laying_year / 10) * 10 AS VARCHAR), '-',
-                      CAST((laying_year / 10) * 10 + 9 AS VARCHAR)) END
+          ELSE CONCAT(CAST(FLOOR(laying_year / 10) * 10 AS INTEGER), '-',
+                      CAST(FLOOR(laying_year / 10) * 10 + 9 AS INTEGER)) END
     </select>
     <select id="selectPipePressureDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
         SELECT COALESCE(CAST(pressure_rating AS VARCHAR), '未填报') name, COUNT(*) "value", '条' unit FROM app_user.water_pipe_info WHERE del_flag = '0'
@@ -78,7 +90,7 @@
     </select>
 
     <select id="selectStatusDistribution" resultType="com.zksy.WaterSupply.PipeNetworkStatistics.dto.MetricItemDTO">
-        SELECT COALESCE(status, '未填报') name, COUNT(*) "value", '条' unit FROM
+        SELECT COALESCE(status, '未') name, COUNT(*) "value", '条' unit FROM
         <choose><when test="tableType == 'pipe'">app_user.water_pipe_info</when><when test="tableType == 'plant'">app_user.water_plant_info</when><when test="tableType == 'pumpStation'">app_user.water_pump_station</when><when test="tableType == 'source'">app_user.water_source_info</when><otherwise>app_user.water_user_info</otherwise></choose>
         WHERE del_flag = '0' <if test="status != null and status != ''">AND status = #{status}</if> GROUP BY status
     </select>

+ 86 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsExportServiceContractTest.java

@@ -0,0 +1,86 @@
+package com.zksy.WaterSupply.PipeNetworkStatistics;
+
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.PipeNetworkStatisticsDashboardDTO;
+import com.zksy.WaterSupply.PipeNetworkStatistics.dto.FacilityStatisticItemDTO;
+import com.zksy.WaterSupply.PipeNetworkStatistics.service.impl.PipeNetworkStatisticsExportServiceImpl;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class PipeNetworkStatisticsExportServiceContractTest {
+    @Test
+    void exportCreatesTheSixFixedWorksheets() throws Exception {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        new PipeNetworkStatisticsExportServiceImpl().write(output, new PipeNetworkStatisticsDashboardDTO());
+
+        try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(output.toByteArray()))) {
+            List<String> sheetNames = new ArrayList<>();
+            for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
+                sheetNames.add(workbook.getSheetName(i));
+            }
+
+            assertEquals(List.of("综合概览", "管网统计", "水厂统计", "泵站统计", "水源地统计", "用水户统计"), sheetNames);
+        }
+    }
+
+    @Test
+    void exportUsesFacilitySpecificStatusLabels() throws Exception {
+        PipeNetworkStatisticsDashboardDTO dashboard = new PipeNetworkStatisticsDashboardDTO();
+        dashboard.setFacilityOverview(Arrays.asList(
+                facility("source", "水源地", 3L, 2L, 1L, 0L, 0L),
+                facility("plant", "水厂", 3L, 2L, 1L, 0L, 0L),
+                facility("pumpStation", "泵站", 4L, 2L, 1L, 1L, 0L),
+                facility("pipe", "供水管网", 3L, 2L, 1L, 0L, 0L),
+                facility("user", "用水户", 4L, 2L, 1L, 1L, 0L)
+        ));
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        new PipeNetworkStatisticsExportServiceImpl().write(output, dashboard);
+
+        try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(output.toByteArray()))) {
+            var sheet = workbook.getSheet("综合概览");
+            assertOverviewRow(sheet, "水源地-正常", 2D);
+            assertOverviewRow(sheet, "水源地-异常", 1D);
+            assertOverviewRow(sheet, "水厂-停产", 1D);
+            assertOverviewRow(sheet, "泵站-故障停运", 1D);
+            assertOverviewRow(sheet, "泵站-检修中", 1D);
+            assertOverviewRow(sheet, "供水管网-停用", 1D);
+            assertOverviewRow(sheet, "用水户-欠费", 1D);
+            assertOverviewRow(sheet, "用水户-停用", 1D);
+        }
+    }
+
+    private FacilityStatisticItemDTO facility(
+            String type, String name, Long total, Long normal, Long abnormal, Long warning, Long unknown
+    ) {
+        FacilityStatisticItemDTO item = new FacilityStatisticItemDTO();
+        item.setFacilityType(type);
+        item.setFacilityTypeName(name);
+        item.setTotalCount(total);
+        item.setNormalCount(normal);
+        item.setAbnormalCount(abnormal);
+        item.setWarningCount(warning);
+        item.setUnknownCount(unknown);
+        return item;
+    }
+
+    private void assertOverviewRow(Sheet sheet, String label, double expectedValue) {
+        for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
+            var row = sheet.getRow(rowIndex);
+            if (row != null && label.equals(row.getCell(1).getStringCellValue())) {
+                assertEquals(expectedValue, row.getCell(2).getNumericCellValue());
+                return;
+            }
+        }
+        fail("Missing export status row: " + label);
+    }
+}

+ 72 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsServiceContractTest.java

@@ -0,0 +1,72 @@
+package com.zksy.WaterSupply.PipeNetworkStatistics;
+
+import com.zksy.WaterSupply.PipeNetworkStatistics.mapper.PipeNetworkStatisticsMapper;
+import com.zksy.WaterSupply.PipeNetworkStatistics.service.impl.PipeNetworkStatisticsServiceImpl;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class PipeNetworkStatisticsServiceContractTest {
+    @Mock
+    private PipeNetworkStatisticsMapper mapper;
+
+    @InjectMocks
+    private PipeNetworkStatisticsServiceImpl service;
+
+    @Test
+    void emptyDatabaseStillReturnsStableZeroDashboardSections() {
+        when(mapper.selectFacilityOverview(null)).thenReturn(null);
+        when(mapper.selectPipeSummary(null)).thenReturn(null);
+        when(mapper.selectWaterPlantSummary(null)).thenReturn(null);
+        when(mapper.selectPumpStationSummary(null)).thenReturn(null);
+        when(mapper.selectWaterSourceSummary(null)).thenReturn(null);
+        when(mapper.selectWaterUserSummary(null)).thenReturn(null);
+
+        var result = service.getDashboard(null, null);
+
+        assertEquals(5, result.getFacilityOverview().size());
+        assertEquals(0L, result.getFacilityOverview().get(0).getTotalCount());
+        assertEquals(0L, result.getFacilityOverview().get(0).getNormalCount());
+        assertEquals(0L, result.getFacilityOverview().get(0).getAbnormalCount());
+        assertEquals(0L, result.getFacilityOverview().get(0).getWarningCount());
+        assertEquals(0L, result.getFacilityOverview().get(0).getUnknownCount());
+        assertNotNull(result.getPipeNetwork());
+        assertEquals(0L, result.getPipeNetwork().get("totalCount"));
+        assertEquals(0L, result.getWaterPlant().get("totalCount"));
+        assertEquals(0L, result.getPumpStation().get("totalCount"));
+        assertEquals(0L, result.getWaterSource().get("totalCount"));
+        assertEquals(0L, result.getWaterUser().get("totalCount"));
+    }
+
+    @Test
+    void facilityOverviewCountsUnexpectedStatusValuesAsUnknown() throws Exception {
+        String mapper = Files.readString(Path.of(
+                "src/main/resources/mapper/WaterSupply/PipeNetworkStatistics/PipeNetworkStatisticsMapper.xml"
+        ));
+
+        assertEquals(3, countOccurrences(mapper, "status IS NULL OR status NOT IN ('0', '1')"));
+        assertEquals(2, countOccurrences(mapper, "status IS NULL OR status NOT IN ('0', '1', '2')"));
+        assertEquals(1, countOccurrences(mapper, "COALESCE(status, '未知') name"));
+    }
+
+    private int countOccurrences(String source, String token) {
+        int count = 0;
+        int index = 0;
+        while ((index = source.indexOf(token, index)) >= 0) {
+            count++;
+            index += token.length();
+        }
+        return count;
+    }
+}

+ 222 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/WaterFacilityImportServiceTest.java

@@ -0,0 +1,222 @@
+package com.zksy.WaterSupply;
+
+import com.zksy.WaterSupply.WaterPipeInfo.domain.WaterPipeInfo;
+import com.zksy.WaterSupply.WaterPipeInfo.service.IWaterPipeInfoService;
+import com.zksy.WaterSupply.WaterPlantInfo.domain.WaterPlantInfo;
+import com.zksy.WaterSupply.WaterPlantInfo.service.IWaterPlantInfoService;
+import com.zksy.WaterSupply.WaterPumpStation.domain.WaterPumpStation;
+import com.zksy.WaterSupply.WaterPumpStation.service.IWaterPumpStationService;
+import com.zksy.WaterSupply.WaterSourceInfo.domain.WaterSourceInfo;
+import com.zksy.WaterSupply.WaterSourceInfo.service.IWaterSourceInfoService;
+import com.zksy.WaterSupply.WaterUserInfo.domain.WaterUserInfo;
+import com.zksy.WaterSupply.WaterUserInfo.service.IWaterUserInfoService;
+import com.zksy.WaterSupply.export.WaterPipeExportDTO;
+import com.zksy.WaterSupply.export.WaterPlantExportDTO;
+import com.zksy.WaterSupply.export.WaterPumpStationExportDTO;
+import com.zksy.WaterSupply.export.WaterSourceExportDTO;
+import com.zksy.WaterSupply.export.WaterUserExportDTO;
+import com.zksy.WaterSupply.importing.WaterFacilityImportService;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class WaterFacilityImportServiceTest {
+
+    @Test
+    void sourceImportCreatesRetrievableCoordinatesAndAuditFields() {
+        IWaterSourceInfoService sourceService = mock(IWaterSourceInfoService.class);
+        when(sourceService.getOne(any(), eq(false))).thenReturn(null);
+        when(sourceService.save(any())).thenReturn(true);
+        WaterFacilityImportService service = serviceWith(sourceService);
+
+        WaterSourceExportDTO row = new WaterSourceExportDTO();
+        row.setSourceId("SRC-IMPORT-001");
+        row.setSourceName("东湖水源地");
+        row.setLongitude(new BigDecimal("112.12345678"));
+        row.setLatitude(new BigDecimal("28.12345678"));
+
+        String result = service.importSources(List.of(row), false, "tester");
+
+        ArgumentCaptor<WaterSourceInfo> saved = ArgumentCaptor.forClass(WaterSourceInfo.class);
+        verify(sourceService).save(saved.capture());
+        assertEquals("SRC-IMPORT-001", saved.getValue().getSourceId());
+        assertEquals(new BigDecimal("112.12345678"), saved.getValue().getLongitude());
+        assertEquals(new BigDecimal("28.12345678"), saved.getValue().getLatitude());
+        assertEquals("0", saved.getValue().getStatus());
+        assertEquals("tester", saved.getValue().getCreateBy());
+        assertEquals("成功导入 1 条水源地数据", result);
+    }
+
+    @Test
+    void plantImportCreatesRetrievableCoordinates() {
+        IWaterPlantInfoService plantService = mock(IWaterPlantInfoService.class);
+        when(plantService.getOne(any(), eq(false))).thenReturn(null);
+        when(plantService.save(any())).thenReturn(true);
+        WaterFacilityImportService service = serviceWith(plantService);
+
+        WaterPlantExportDTO row = new WaterPlantExportDTO();
+        row.setPlantCode("PLANT-IMPORT-001");
+        row.setPlantName("第一水厂");
+        row.setLongitude(new BigDecimal("112.33333333"));
+        row.setLatitude(new BigDecimal("28.33333333"));
+
+        String result = service.importPlants(List.of(row), false, "tester");
+
+        ArgumentCaptor<WaterPlantInfo> saved = ArgumentCaptor.forClass(WaterPlantInfo.class);
+        verify(plantService).save(saved.capture());
+        assertEquals("PLANT-IMPORT-001", saved.getValue().getPlantCode());
+        assertEquals(new BigDecimal("112.33333333"), saved.getValue().getLongitude());
+        assertEquals(new BigDecimal("28.33333333"), saved.getValue().getLatitude());
+        assertEquals("成功导入 1 条水厂数据", result);
+    }
+
+    @Test
+    void pumpImportCreatesRetrievableCoordinates() {
+        IWaterPumpStationService pumpService = mock(IWaterPumpStationService.class);
+        when(pumpService.getOne(any(), eq(false))).thenReturn(null);
+        when(pumpService.save(any())).thenReturn(true);
+        WaterFacilityImportService service = serviceWith(pumpService);
+
+        WaterPumpStationExportDTO row = new WaterPumpStationExportDTO();
+        row.setStationCode("PUMP-IMPORT-001");
+        row.setStationName("加压泵站");
+        row.setLongitude(new BigDecimal("112.44444444"));
+        row.setLatitude(new BigDecimal("28.44444444"));
+
+        String result = service.importPumps(List.of(row), false, "tester");
+
+        ArgumentCaptor<WaterPumpStation> saved = ArgumentCaptor.forClass(WaterPumpStation.class);
+        verify(pumpService).save(saved.capture());
+        assertEquals("PUMP-IMPORT-001", saved.getValue().getStationCode());
+        assertEquals(new BigDecimal("112.44444444"), saved.getValue().getLongitude());
+        assertEquals(new BigDecimal("28.44444444"), saved.getValue().getLatitude());
+        assertEquals("成功导入 1 条泵站数据", result);
+    }
+
+    @Test
+    void pipeImportRetainsStartAndEndCoordinates() {
+        IWaterPipeInfoService pipeService = mock(IWaterPipeInfoService.class);
+        when(pipeService.getOne(any(), eq(false))).thenReturn(null);
+        when(pipeService.save(any())).thenReturn(true);
+        WaterFacilityImportService service = new WaterFacilityImportService(
+                mock(IWaterSourceInfoService.class),
+                mock(IWaterPlantInfoService.class),
+                mock(IWaterPumpStationService.class),
+                pipeService,
+                mock(IWaterUserInfoService.class));
+
+        WaterPipeExportDTO row = new WaterPipeExportDTO();
+        row.setPipeCode("PIPE-IMPORT-001");
+        row.setPipeName("输水干线");
+        row.setStartLongitude(new BigDecimal("112.11111111"));
+        row.setStartLatitude(new BigDecimal("28.11111111"));
+        row.setEndLongitude(new BigDecimal("112.22222222"));
+        row.setEndLatitude(new BigDecimal("28.22222222"));
+
+        String result = service.importPipes(List.of(row), false, "tester");
+
+        ArgumentCaptor<WaterPipeInfo> saved = ArgumentCaptor.forClass(WaterPipeInfo.class);
+        verify(pipeService).save(saved.capture());
+        assertEquals("PIPE-IMPORT-001", saved.getValue().getPipeCode());
+        assertEquals(new BigDecimal("112.11111111"), saved.getValue().getStartLongitude());
+        assertEquals(new BigDecimal("28.11111111"), saved.getValue().getStartLatitude());
+        assertEquals(new BigDecimal("112.22222222"), saved.getValue().getEndLongitude());
+        assertEquals(new BigDecimal("28.22222222"), saved.getValue().getEndLatitude());
+        assertEquals("成功导入 1 条管网数据", result);
+    }
+
+    @Test
+    void userImportCreatesRetrievableCoordinates() {
+        IWaterUserInfoService userService = mock(IWaterUserInfoService.class);
+        when(userService.getOne(any(), eq(false))).thenReturn(null);
+        when(userService.save(any())).thenReturn(true);
+        WaterFacilityImportService service = serviceWith(userService);
+
+        WaterUserExportDTO row = new WaterUserExportDTO();
+        row.setUserCode("USER-IMPORT-001");
+        row.setUserName("园区工业用水户");
+        row.setLongitude(new BigDecimal("112.55555555"));
+        row.setLatitude(new BigDecimal("28.55555555"));
+
+        String result = service.importUsers(List.of(row), false, "tester");
+
+        ArgumentCaptor<WaterUserInfo> saved = ArgumentCaptor.forClass(WaterUserInfo.class);
+        verify(userService).save(saved.capture());
+        assertEquals("USER-IMPORT-001", saved.getValue().getUserCode());
+        assertEquals(new BigDecimal("112.55555555"), saved.getValue().getLongitude());
+        assertEquals(new BigDecimal("28.55555555"), saved.getValue().getLatitude());
+        assertEquals("成功导入 1 条用水户数据", result);
+    }
+
+    @Test
+    void existingRowsAreSkippedUntilUpdateSupportIsEnabled() {
+        IWaterSourceInfoService sourceService = mock(IWaterSourceInfoService.class);
+        WaterSourceInfo existing = new WaterSourceInfo();
+        existing.setSourceId("SRC-IMPORT-001");
+        when(sourceService.getOne(any(), eq(false))).thenReturn(existing);
+        WaterFacilityImportService service = serviceWith(sourceService);
+
+        WaterSourceExportDTO row = new WaterSourceExportDTO();
+        row.setSourceId("SRC-IMPORT-001");
+        row.setLongitude(new BigDecimal("112.66666666"));
+        row.setLatitude(new BigDecimal("28.66666666"));
+
+        assertEquals("成功导入 0 条水源地数据", service.importSources(List.of(row), false, "tester"));
+        verify(sourceService, never()).save(any());
+        verify(sourceService, never()).updateById(any());
+
+        when(sourceService.updateById(any())).thenReturn(true);
+        assertEquals("成功导入 1 条水源地数据", service.importSources(List.of(row), true, "tester"));
+
+        ArgumentCaptor<WaterSourceInfo> updated = ArgumentCaptor.forClass(WaterSourceInfo.class);
+        verify(sourceService).updateById(updated.capture());
+        assertEquals("tester", updated.getValue().getUpdateBy());
+        assertEquals(new BigDecimal("112.66666666"), updated.getValue().getLongitude());
+        assertEquals(new BigDecimal("28.66666666"), updated.getValue().getLatitude());
+    }
+
+    private static WaterFacilityImportService serviceWith(IWaterPlantInfoService plantService) {
+        return new WaterFacilityImportService(
+                mock(IWaterSourceInfoService.class),
+                plantService,
+                mock(IWaterPumpStationService.class),
+                mock(IWaterPipeInfoService.class),
+                mock(IWaterUserInfoService.class));
+    }
+
+    private static WaterFacilityImportService serviceWith(IWaterPumpStationService pumpService) {
+        return new WaterFacilityImportService(
+                mock(IWaterSourceInfoService.class),
+                mock(IWaterPlantInfoService.class),
+                pumpService,
+                mock(IWaterPipeInfoService.class),
+                mock(IWaterUserInfoService.class));
+    }
+
+    private static WaterFacilityImportService serviceWith(IWaterUserInfoService userService) {
+        return new WaterFacilityImportService(
+                mock(IWaterSourceInfoService.class),
+                mock(IWaterPlantInfoService.class),
+                mock(IWaterPumpStationService.class),
+                mock(IWaterPipeInfoService.class),
+                userService);
+    }
+    private static WaterFacilityImportService serviceWith(IWaterSourceInfoService sourceService) {
+        return new WaterFacilityImportService(
+                sourceService,
+                mock(IWaterPlantInfoService.class),
+                mock(IWaterPumpStationService.class),
+                mock(IWaterPipeInfoService.class),
+                mock(IWaterUserInfoService.class));
+    }
+}