battery-business/src/main/java/com/battery/business/service/impl/DeviceAdapterServiceImpl.java
@@ -5,6 +5,8 @@ import java.time.ZoneId; import java.util.List; import java.util.Locale; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import com.alibaba.fastjson2.JSON; @@ -28,6 +30,7 @@ @Service public class DeviceAdapterServiceImpl implements IDeviceAdapterService { private static final Logger log = LoggerFactory.getLogger(DeviceAdapterServiceImpl.class); private static final String ENABLED = "0"; private static final String DISABLED = "1"; private static final String READ_ONLY_CAPABILITIES = @@ -122,10 +125,11 @@ TDeviceAdapter adapter = selectById(adapterId); String status = enabled ? ENABLED : DISABLED; if (status.equals(adapter.getStatus())) return 1; int rows = mapper.updateDeviceAdapterStatus(adapterId, status, updateBy); adapter.setStatus(status); // 先装配运行时实例:装配失败(例如缺少口令环境变量)时不把状态写成"已启用", // 免得留下"库里启用、运行时没有实例"的假状态。 registry.synchronize(toRuntimeConfig(adapter)); return rows; return mapper.updateDeviceAdapterStatus(adapterId, status, updateBy); } /** 执行一次发现和实时观测,将未登记设备写入 Redis 候选。 */ @@ -184,7 +188,19 @@ @Override public void synchronizeEnabledAdapters() { mapper.selectEnabledDeviceAdapters().forEach(adapter -> registry.synchronize(toRuntimeConfig(adapter))); for (TDeviceAdapter adapter : mapper.selectEnabledDeviceAdapters()) { try { registry.synchronize(toRuntimeConfig(adapter)); } catch (RuntimeException exception) { // 单行配置有问题(例如缺少口令环境变量)不得拦住后端启动: // 记下原因,由发现轮询按周期重试并把失败持久化到这一行。 log.warn("设备适配器实例装配失败 adapterCode={}:{}", adapter.getAdapterCode(), exception.getMessage()); } } } /** 轮询达到周期的已启用适配器,并持久化失败状态后继续调度。 */ battery-business/src/main/resources/mapper/battery/TDeviceAdapterMapper.xml
@@ -84,7 +84,7 @@ <update id="updateDeviceAdapter" parameterType="TDeviceAdapter"> update t_device_adapter set adapter_name=#{adapterName}, adapter_type=#{adapterType}, host=#{host}, port=#{port}, poll_interval_ms=#{pollIntervalMs}, timezone_id=#{timezoneId}, capabilities=#{capabilities}, source_config=cast(#{sourceConfig} as json), source_config=cast(#{sourceConfig} as json), remark=#{remark}, connection_status='OFFLINE', last_error_code=null, last_error_message=null, update_by=#{updateBy}, update_time=sysdate() where adapter_id=#{adapterId} and del_flag=0 ruoyi-admin/src/test/java/com/battery/business/service/impl/DeviceAdapterServiceImplTest.java
@@ -94,6 +94,46 @@ verify(registry).synchronize(any()); } /** 装配失败(例如缺少口令环境变量)时不能把状态改成"已启用",免得留下库里启用、运行时没实例的假状态。 */ @Test void failedEnableKeepsAdapterDisabledAndSurfacesTheReason() { adapter.setStatus("1"); when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter); org.mockito.Mockito.doThrow(new DeviceAdapterException("缺少厂商库连接口令:请设置环境变量 X")) .when(registry).synchronize(any()); DeviceAdapterException exception = assertThrows(DeviceAdapterException.class, () -> service.changeStatus(7L, true, "admin")); assertTrue(exception.getMessage().contains("缺少厂商库连接口令")); verify(adapterMapper, never()).updateDeviceAdapterStatus(any(), any(), any()); } /** 单行配置有问题不得拦住后端启动:记下原因,由发现轮询按周期重试并持久化失败。 */ @Test void startupRestoreSurvivesOneBrokenAdapterRow() { TDeviceAdapter vendor = new TDeviceAdapter(); vendor.setAdapterId(9L); vendor.setAdapterCode("neware76"); vendor.setAdapterName("新威 7.6 那台"); vendor.setAdapterType("NEWARE_MYSQL"); vendor.setHost("26.70.208.249"); vendor.setPort(3306); vendor.setPollIntervalMs(60000); vendor.setTimezoneId("Asia/Hong_Kong"); vendor.setStatus("0"); vendor.setCapabilities("{\"discovery\":true,\"realtimeObservation\":true,\"historyDownload\":true,\"control\":false}"); when(adapterMapper.selectEnabledDeviceAdapters()).thenReturn(List.of(vendor)); org.mockito.Mockito.doThrow(new DeviceAdapterException("缺少厂商库连接口令:请设置环境变量 X")) .when(registry).synchronize(any()); service.synchronizeEnabledAdapters(); verify(registry).synchronize(any()); } @Test void springSelectsProductionConstructor() {