Selaa lähdekoodia

feat(water): support device ledger import and image

Kazerin 12 tuntia sitten
vanhempi
commit
d9f576f8ab

+ 18 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/WaterSupply/DeviceLedger/DeviceLedgerController.java

@@ -5,6 +5,8 @@ import com.zksy.base.domain.vo.DeviceGisVO;
 import com.zksy.base.domain.vo.DeviceTypeStatisticsVO;
 import com.zksy.base.service.WaterSupplyDeviceService;
 import com.zksy.WaterSupply.export.WaterDeviceLedgerExportDTO;
+import com.zksy.WaterSupply.importing.WaterDeviceLedgerImportDTO;
+import com.zksy.WaterSupply.importing.WaterDeviceLedgerImportService;
 import com.zksy.common.annotation.Log;
 import com.zksy.common.core.domain.AjaxResult;
 import com.zksy.common.enums.BusinessType;
@@ -16,6 +18,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletResponse;
 import java.util.List;
@@ -30,6 +33,9 @@ public class DeviceLedgerController {
     @Autowired
     private WaterSupplyDeviceService waterSupplyDeviceService;
 
+    @Autowired
+    private WaterDeviceLedgerImportService waterDeviceLedgerImportService;
+
     @GetMapping("/typeStatistics")
     @ApiOperation("设备分类统计(树形结构)")
     @PreAuthorize("@ss.hasPermi('waterSupply:device:list')")
@@ -83,4 +89,16 @@ public class DeviceLedgerController {
                 .collect(Collectors.toList());
         new ExcelUtil<>(WaterDeviceLedgerExportDTO.class).exportExcel(response, rows, "供水设备台账");
     }
+
+    @PostMapping("/import")
+    @ApiOperation("批量导入设备台账")
+    @Log(title = "导入设备台账", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('waterSupply:device:list')")
+    public AjaxResult importDeviceLedger(
+            @ApiParam("Excel 文件") @RequestParam("file") MultipartFile file,
+            @ApiParam("是否更新已存在设备") @RequestParam(defaultValue = "false") boolean updateSupport) throws Exception {
+        List<WaterDeviceLedgerImportDTO> rows = new ExcelUtil<>(WaterDeviceLedgerImportDTO.class)
+                .importExcel(file.getInputStream());
+        return AjaxResult.success(waterDeviceLedgerImportService.importDevices(rows, updateSupport));
+    }
 }

+ 4 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/WaterSupply/export/WaterDeviceLedgerExportDTO.java

@@ -20,6 +20,9 @@ public class WaterDeviceLedgerExportDTO {
     @Excel(name = "安装位置")
     private String equipmentLocation;
 
+    @Excel(name = "设备图片URL")
+    private String equipmentImage;
+
     @Excel(name = "设备状态", readConverterExp = "1=在用,2=闲置,3=维修,4=报废,5=待入库")
     private Integer currentStatus;
 
@@ -41,6 +44,7 @@ public class WaterDeviceLedgerExportDTO {
         dto.equipmentName = value.getEquipmentName();
         dto.equipmentTypeName = value.getEquipmentTypeName();
         dto.equipmentLocation = value.getEquipmentLocation();
+        dto.equipmentImage = value.getEquipmentImage();
         dto.currentStatus = value.getCurrentStatus();
         dto.onlineStatus = value.getOnlineStatus();
         dto.alarmStatus = value.getAlarmStatus();

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

@@ -0,0 +1,55 @@
+package com.zksy.WaterSupply.importing;
+
+import com.zksy.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/** 供水设备台账 Excel 导入行。 */
+@Data
+public class WaterDeviceLedgerImportDTO {
+    @Excel(name = "设备编码")
+    private String equipmentCode;
+
+    @Excel(name = "设备名称")
+    private String equipmentName;
+
+    @Excel(name = "设备类型")
+    private String equipmentTypeName;
+
+    @Excel(name = "设备型号")
+    private String equipmentModel;
+
+    @Excel(name = "设备规格参数")
+    private String equipmentSpec;
+
+    @Excel(name = "制造商")
+    private String manufacturer;
+
+    @Excel(name = "所属片区", readConverterExp = "CN=城南,CB=城北,TC=太常,城南=城南,城北=城北,太常=太常")
+    private String district;
+
+    @Excel(name = "经度")
+    private BigDecimal longitude;
+
+    @Excel(name = "纬度")
+    private BigDecimal latitude;
+
+    @Excel(name = "安装位置")
+    private String equipmentLocation;
+
+    @Excel(name = "设备负责人")
+    private String maintainer;
+
+    @Excel(name = "联系方式")
+    private String maintainerPhone;
+
+    @Excel(name = "权属单位")
+    private String ownershipUnit;
+
+    @Excel(name = "备注")
+    private String remark;
+
+    @Excel(name = "设备图片URL")
+    private String equipmentImage;
+}

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

@@ -0,0 +1,126 @@
+package com.zksy.WaterSupply.importing;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.bean.copier.CopyOptions;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.mapper.EquipmentTypeMapper;
+import com.zksy.base.service.EquipmentBaseService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/** 供水设备台账 Excel 导入服务;设备类型必须归属供水顶级类型。 */
+@Service
+@Transactional(rollbackFor = Exception.class)
+public class WaterDeviceLedgerImportService {
+    private static final String WATER_TOP_TYPE_NAME = "供水";
+
+    private final EquipmentBaseService equipmentBaseService;
+    private final EquipmentTypeMapper equipmentTypeMapper;
+
+    public WaterDeviceLedgerImportService(EquipmentBaseService equipmentBaseService,
+                                          EquipmentTypeMapper equipmentTypeMapper) {
+        this.equipmentBaseService = equipmentBaseService;
+        this.equipmentTypeMapper = equipmentTypeMapper;
+    }
+
+    public String importDevices(List<WaterDeviceLedgerImportDTO> rows, boolean updateSupport) {
+        if (rows == null || rows.isEmpty()) {
+            throw new IllegalArgumentException("导入文件不能为空");
+        }
+
+        Set<String> seenCodes = new HashSet<>();
+        int count = 0;
+        for (int index = 0; index < rows.size(); index++) {
+            WaterDeviceLedgerImportDTO row = rows.get(index);
+            String code = StrUtil.trimToNull(row.getEquipmentCode());
+            if (code == null) {
+                throw new IllegalArgumentException("导入失败:第 " + (index + 1) + " 行设备编码不能为空");
+            }
+            if (!seenCodes.add(code)) {
+                throw new IllegalArgumentException("导入失败:设备编码重复:" + code);
+            }
+
+            String typeName = StrUtil.trimToNull(row.getEquipmentTypeName());
+            if (typeName == null) {
+                throw new IllegalArgumentException("导入失败:设备编码 " + code + " 的设备类型不能为空");
+            }
+            String waterTypeId = resolveWaterTypeId(typeName);
+
+            EquipmentBase existing = equipmentBaseService.getOne(
+                    Wrappers.<EquipmentBase>lambdaQuery().eq(EquipmentBase::getEquipmentCode, code), false);
+            boolean update = existing != null;
+            if (update && !updateSupport) {
+                continue;
+            }
+
+            EquipmentBase device = update ? existing : new EquipmentBase();
+            BeanUtil.copyProperties(row, device, CopyOptions.create().setIgnoreNullValue(true));
+            device.setEquipmentCode(code);
+            device.setEquipmentTypeId(waterTypeId);
+            LocalDateTime now = LocalDateTime.now();
+            if (update) {
+                device.setUpdateTime(now);
+                if (!equipmentBaseService.updateById(device)) {
+                    throw new IllegalStateException("供水设备数据更新失败:" + code);
+                }
+            } else {
+                device.setCreateTime(now);
+                device.setUpdateTime(now);
+                if (!equipmentBaseService.save(device)) {
+                    throw new IllegalStateException("供水设备数据保存失败:" + code);
+                }
+            }
+            count++;
+        }
+        return "成功导入 " + count + " 台供水设备";
+    }
+
+    private String resolveWaterTypeId(String typeName) {
+        List<EquipmentType> allTypes = equipmentTypeMapper.selectList(Wrappers.emptyWrapper());
+        Map<String, EquipmentType> typesById = allTypes.stream()
+                .collect(Collectors.toMap(EquipmentType::getId, Function.identity(), (left, right) -> left));
+        List<EquipmentType> matches = allTypes.stream()
+                .filter(type -> typeName.equals(type.getTypeName()))
+                .collect(Collectors.toList());
+        if (matches.isEmpty()) {
+            throw new IllegalArgumentException("导入失败:设备类型不存在:" + typeName);
+        }
+
+        Set<String> waterTypeIds = matches.stream()
+                .filter(type -> isWaterType(type, typesById))
+                .map(EquipmentType::getId)
+                .collect(Collectors.toSet());
+        if (waterTypeIds.isEmpty()) {
+            throw new IllegalArgumentException("导入失败:设备类型不属于供水:" + typeName);
+        }
+        if (waterTypeIds.size() > 1) {
+            throw new IllegalArgumentException("导入失败:存在多个同名供水设备类型:" + typeName);
+        }
+        return waterTypeIds.iterator().next();
+    }
+
+    private boolean isWaterType(EquipmentType type, Map<String, EquipmentType> typesById) {
+        EquipmentType cursor = type;
+        int guard = 0;
+        while (cursor != null && guard++ <= typesById.size()) {
+            if (WATER_TOP_TYPE_NAME.equals(cursor.getTypeName())
+                    && "0".equals(cursor.getParentTypeId())) {
+                return true;
+            }
+            cursor = typesById.get(cursor.getParentTypeId());
+        }
+        return false;
+    }
+}

+ 3 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/vo/DeviceGisVO.java

@@ -43,6 +43,9 @@ public class DeviceGisVO {
     @ApiModelProperty("设备位置")
     private String equipmentLocation;
 
+    @ApiModelProperty("设备图片URL")
+    private String equipmentImage;
+
     @ApiModelProperty("在线状态 0-离线 1-在线")
     private Integer onlineStatus;
 

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

@@ -479,15 +479,20 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
         Map<String, EquipmentStatus> statusMap = allAbnormalStatus.stream()
                 .collect(Collectors.toMap(EquipmentStatus::getEquipmentId, s -> s, (a, b) -> a));
 
+        Map<String, AlarmData> activeAlarmMap = getActiveAlarmMap(waterDevices);
         List<EquipmentBase> allDevices = waterDevices.stream()
-                .filter(device -> Optional.ofNullable(statusMap.get(device.getEquipmentId()))
-                        .map(status -> Objects.equals(status.getAlarmStatus(), 1)
-                                || Objects.equals(status.getCurrentStatus(), 3))
-                        .orElse(false))
+                .filter(device -> {
+                    EquipmentStatus status = statusMap.get(device.getEquipmentId());
+                    if (status == null) {
+                        return activeAlarmMap.containsKey(device.getEquipmentCode());
+                    }
+                    return Objects.equals(status.getAlarmStatus(), 1)
+                            || Objects.equals(status.getCurrentStatus(), 3);
+                })
                 .filter(device -> matchesLedgerFilters(device, equipmentName, equipmentCode, equipmentTypeId))
                 .collect(Collectors.toList());
 
-        Map<String, AlarmData> alarmMap = getActiveAlarmMap(allDevices);
+        Map<String, AlarmData> alarmMap = activeAlarmMap;
         Map<String, WorkOrder> workOrderMap = getLatestWorkOrderMap(allDevices);
         Map<String, EquipmentMaintain> maintainMap = getLatestMaintainMap(
                 allDevices.stream().map(EquipmentBase::getEquipmentId).collect(Collectors.toList())
@@ -859,7 +864,7 @@ public class WaterSupplyDeviceServiceImpl extends ServiceImpl<EquipmentBaseMappe
     }
 
     private Integer resolveAlertLevel(EquipmentBase device, EquipmentStatus status, Map<String, AlarmData> alarmMap) {
-        if (status == null || status.getAlarmStatus() == null || status.getAlarmStatus() == 0) {
+        if (status != null && !Objects.equals(status.getAlarmStatus(), 1)) {
             return null;
         }
         AlarmData alarm = alarmMap.get(device.getEquipmentCode());

+ 91 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/WaterSupply/importing/WaterDeviceLedgerImportServiceTest.java

@@ -0,0 +1,91 @@
+package com.zksy.WaterSupply.importing;
+
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.domain.EquipmentType;
+import com.zksy.base.mapper.EquipmentTypeMapper;
+import com.zksy.base.service.EquipmentBaseService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+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 WaterDeviceLedgerImportServiceTest {
+    private WaterDeviceLedgerImportService service;
+    private EquipmentBaseService equipmentBaseService;
+    private EquipmentTypeMapper equipmentTypeMapper;
+
+    @BeforeEach
+    void setUp() {
+        equipmentBaseService = mock(EquipmentBaseService.class);
+        equipmentTypeMapper = mock(EquipmentTypeMapper.class);
+        service = new WaterDeviceLedgerImportService(equipmentBaseService, equipmentTypeMapper);
+    }
+    @Test
+    void importDevicesSavesWaterTypeWithEquipmentImage() {
+        EquipmentType waterRoot = type("db-water-root", "water-root", "供水", "0");
+        EquipmentType pressureType = type("db-water-pressure", "water-pressure", "压力计", "db-water-root");
+        EquipmentType gasRoot = type("db-gas-root", "gas-root", "燃气", "0");
+        when(equipmentTypeMapper.selectList(any())).thenReturn(Arrays.asList(waterRoot, pressureType, gasRoot));
+        when(equipmentBaseService.getOne(any(), eq(false))).thenReturn(null);
+        when(equipmentBaseService.save(any(EquipmentBase.class))).thenReturn(true);
+
+        WaterDeviceLedgerImportDTO row = new WaterDeviceLedgerImportDTO();
+        row.setEquipmentCode(" CN-GS-202609050001 ");
+        row.setEquipmentName("导入压力计");
+        row.setEquipmentTypeName("压力计");
+        row.setEquipmentLocation("测试位置");
+        row.setLongitude(new BigDecimal("113.28"));
+        row.setLatitude(new BigDecimal("23.12"));
+        row.setEquipmentImage("https://example.com/device.jpg");
+
+        assertEquals("成功导入 1 台供水设备", service.importDevices(Collections.singletonList(row), false));
+
+        ArgumentCaptor<EquipmentBase> captor = ArgumentCaptor.forClass(EquipmentBase.class);
+        verify(equipmentBaseService).save(captor.capture());
+        assertEquals("CN-GS-202609050001", captor.getValue().getEquipmentCode());
+        assertEquals("db-water-pressure", captor.getValue().getEquipmentTypeId());
+        assertEquals("https://example.com/device.jpg", captor.getValue().getEquipmentImage());
+        verify(equipmentBaseService, never()).updateById(any(EquipmentBase.class));
+    }
+
+    @Test
+    void importDevicesRejectsNonWaterTypeWithoutSaving() {
+        EquipmentType waterRoot = type("db-water-root", "water-root", "供水", "0");
+        EquipmentType gasChild = type("db-gas-pressure", "gas-pressure", "燃气压力计", "db-gas-root");
+        EquipmentType gasRoot = type("db-gas-root", "gas-root", "燃气", "0");
+        when(equipmentTypeMapper.selectList(any())).thenReturn(Arrays.asList(waterRoot, gasChild, gasRoot));
+
+        WaterDeviceLedgerImportDTO row = new WaterDeviceLedgerImportDTO();
+        row.setEquipmentCode("CN-RQ-202609050001");
+        row.setEquipmentName("燃气压力计");
+        row.setEquipmentTypeName("燃气压力计");
+
+        IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
+                () -> service.importDevices(Collections.singletonList(row), true));
+
+        assertEquals("导入失败:设备类型不属于供水:燃气压力计", error.getMessage());
+        verify(equipmentBaseService, never()).save(any(EquipmentBase.class));
+        verify(equipmentBaseService, never()).updateById(any(EquipmentBase.class));
+    }
+
+    private static EquipmentType type(String id, String typeId, String name, String parentId) {
+        EquipmentType value = new EquipmentType();
+        value.setId(id);
+        value.setTypeId(typeId);
+        value.setTypeName(name);
+        value.setParentTypeId(parentId);
+        return value;
+    }
+}

+ 31 - 0
pipe-network-service/zksy-system/src/test/java/com/zksy/base/service/impl/WaterSupplyDeviceServiceImplTest.java

@@ -142,6 +142,21 @@ class WaterSupplyDeviceServiceImplTest {
         assertEquals("business-pressure", result.getRecords().get(0).getEquipmentTypeName());
     }
 
+    @Test
+    void ledgerPageReturnsEquipmentImage() {
+        pressureDevice.setEquipmentImage("https://example.com/pressure-device.jpg");
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(
+                status(pressureDevice.getEquipmentId(), 1, 0, 1)));
+        when(equipmentTypeMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(
+                type("water-pressure", "water-pressure", "water-root")));
+
+        Page<DeviceGisVO> result = service.getDeviceLedgerPage(
+                1, 10, null, null, null, null, null, null);
+
+        assertEquals("https://example.com/pressure-device.jpg",
+                result.getRecords().get(0).getEquipmentImage());
+    }
+
     @Test
     void ledgerPageAppliesWaterOwnershipAndCurrentStatusFilter() {
         when(equipmentStatusMapper.selectList(any())).thenReturn(Arrays.asList(
@@ -304,6 +319,22 @@ class WaterSupplyDeviceServiceImplTest {
         assertNull(result.get(1).getAlertLevel());
     }
 
+    @Test
+    void abnormalPageIncludesActiveAlarmWhenRuntimeStatusRecordIsMissing() {
+        when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(alarmDataMapper.selectList(any())).thenReturn(Collections.singletonList(
+                alarm(pressureDevice.getEquipmentCode(), "供水压力超限", 1)));
+        when(workOrderMapper.selectList(any())).thenReturn(Collections.emptyList());
+
+        Page<AbnormalDeviceVO> result = service.getAbnormalDevicePage(1, 10, null, null, null, null);
+
+        assertEquals(1, result.getTotal());
+        AbnormalDeviceVO row = result.getRecords().get(0);
+        assertEquals(pressureDevice.getEquipmentId(), row.getEquipmentId());
+        assertEquals(1, row.getAlertLevel());
+        assertEquals("供水压力超限", ReflectionTestUtils.getField(row, "abnormalReason"));
+    }
+
     @Test
     void abnormalAlertLevelFilterUsesActiveAlarmLevel() {
         when(equipmentStatusMapper.selectList(any())).thenReturn(Collections.singletonList(