Browse Source

feat(water): add facility excel import

Kazerin 4 days ago
parent
commit
f11e3bbfda

+ 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 $$;

+ 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, "用水户信息"); }
+}

+ 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);
+    }
 }

+ 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;
+        }
+    }
+}

+ 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));
+    }
+}