| | |
| | | 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; |
| | |
| | | @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 = |
| | | "{\"discovery\":true,\"realtimeObservation\":true,\"historyDownload\":true,\"control\":false}"; |
| | | /** |
| | | * 适配器类型按存储形态划分;BTS 保留给开发模拟器与将来的协议路线(ADR-0065)。 |
| | | * `NEWARE_MYSQL` 只能写字面量:battery-business 不依赖 battery-device-neware, |
| | | * 拿不到那边的常量;推式来源的实现就在本模块,所以用它的常量。 |
| | | */ |
| | | private static final List<String> SUPPORTED_TYPES = List.of("BTS", "NEWARE_MYSQL", PushIngestAdapter.ADAPTER_TYPE); |
| | | /** |
| | | * 通道清单长期存在的来源,候选不按时间过期:直连厂商存储的来源读的是厂商的登记表, |
| | | * 推式来源读的是取数程序上报的拓扑——两者都不是"轮询发现"的结果。 |
| | | */ |
| | | private static final List<String> PERSISTENT_CANDIDATE_TYPES = |
| | | List.of("NEWARE_MYSQL", PushIngestAdapter.ADAPTER_TYPE); |
| | | |
| | | private final TDeviceAdapterMapper mapper; |
| | | private final DeviceAdapterRegistry registry; |
| | |
| | | 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 候选。 */ |
| | |
| | | registry.synchronize(toRuntimeConfig(adapter)); |
| | | AdapterDiscoveryResult result = persistence.discoverForReception(registry, new DiscoverySource( |
| | | adapter.getAdapterCode(), adapter.getAdapterId(), adapter.getAdapterName(), |
| | | adapter.getPollIntervalMs())); |
| | | adapter.getPollIntervalMs(), PERSISTENT_CANDIDATE_TYPES.contains(adapter.getAdapterType()))); |
| | | mapper.updateDiscoverySuccess(adapterId, result.observedAtUtc()); |
| | | return result; |
| | | } |
| | |
| | | @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()); |
| | | mapper.updateDiscoveryFailure(adapter.getAdapterId(), startupFailureCode(exception), |
| | | exception.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 装配失败写进实例行的错误码:适配器自己给出分类(例如凭据缺失、连接失败)时用它, |
| | | * 否则记为配置问题——启动阶段的装配失败基本都是配置造成的。 |
| | | */ |
| | | private static String startupFailureCode(RuntimeException exception) |
| | | { |
| | | return exception instanceof DeviceAdapterException deviceFailure && !deviceFailure.failureCode().isBlank() |
| | | ? deviceFailure.failureCode() : "INVALID_CONFIG"; |
| | | } |
| | | |
| | | /** 轮询达到周期的已启用适配器,并持久化失败状态后继续调度。 */ |
| | |
| | | adapter.setAdapterType(StringUtils.trim(adapter.getAdapterType()).toUpperCase(Locale.ROOT)); |
| | | adapter.setHost(StringUtils.trim(adapter.getHost())); |
| | | adapter.setTimezoneId(StringUtils.trim(adapter.getTimezoneId())); |
| | | adapter.setSourceConfig(StringUtils.trim(adapter.getSourceConfig())); |
| | | adapter.setRemark(StringUtils.trim(adapter.getRemark())); |
| | | if (!"BTS".equals(adapter.getAdapterType())) |
| | | if (!SUPPORTED_TYPES.contains(adapter.getAdapterType())) |
| | | throw error("不支持的适配器类型", DeviceAdapterErrorCodes.UNSUPPORTED_TYPE); |
| | | if (!Boolean.TRUE.equals(adapter.getReadOnly())) |
| | | throw error("当前里程碑只允许只读设备适配器", DeviceAdapterErrorCodes.INVALID_CONFIG); |
| | |
| | | { |
| | | return new AdapterInstanceConfig(adapter.getAdapterCode(), adapter.getAdapterType(), adapter.getHost(), |
| | | adapter.getPort(), adapter.getPollIntervalMs(), adapter.getTimezoneId(), |
| | | ENABLED.equals(adapter.getStatus()), parseCapabilities(adapter.getCapabilities())); |
| | | ENABLED.equals(adapter.getStatus()), parseCapabilities(adapter.getCapabilities()), |
| | | parseSourceConfig(adapter.getSourceConfig())); |
| | | } |
| | | |
| | | /** 来源形态配置只承载非敏感项;连接口令由环境变量或启动参数提供,不入库。 */ |
| | | private static java.util.Map<String, String> parseSourceConfig(String sourceConfig) |
| | | { |
| | | if (StringUtils.isBlank(sourceConfig)) return java.util.Map.of(); |
| | | JSONObject value = JSON.parseObject(sourceConfig); |
| | | if (value == null) return java.util.Map.of(); |
| | | java.util.Map<String, String> result = new java.util.LinkedHashMap<>(); |
| | | value.forEach((key, item) -> { |
| | | if (item != null) result.put(String.valueOf(key), String.valueOf(item)); |
| | | }); |
| | | return result; |
| | | } |
| | | |
| | | private AdapterCapabilities parseCapabilities(String capabilities) |