yeg
2026-09-29 b78295e8da96c3e5e76befeef9582ce5273dbb74
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,10 +30,23 @@
@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;
@@ -118,10 +133,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 候选。 */
@@ -136,7 +152,7 @@
            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;
        }
@@ -180,7 +196,31 @@
    @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";
    }
    /** 轮询达到周期的已启用适配器,并持久化失败状态后继续调度。 */
@@ -214,8 +254,9 @@
        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);
@@ -243,7 +284,21 @@
    {
        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)