package com.battery.device.neware; import java.nio.file.Path; import java.util.List; import java.util.Locale; import java.util.Map; import com.battery.device.api.DeviceAdapter; import com.battery.device.api.DeviceAdapterException; import com.battery.device.api.credentials.LocalCredentials; import com.battery.device.api.model.AdapterCapabilities; import com.battery.device.api.registry.AdapterInstanceConfig; import com.battery.device.api.registry.DeviceAdapterFactory; /** * 按存储形态 {@code NEWARE_MYSQL} 构造直连厂商库的只读适配器实例。 * *

连接口令不进数据库、不进仓库,也不打包进 jar,取值顺序见 {@link #credential}: * 本地凭据文件 → 同名环境变量 → 同名 JVM 启动参数。三者都没有时装配失败, * 错误信息同时点名文件路径与变量名,使"忘了放口令"是可见的失败而不是沉默的缺失。 * 账号固定为现场取得的服务端只读账号,全权限账号不进入配置。 */ public final class NewareMysqlAdapterFactory implements DeviceAdapterFactory { public static final String DATABASE_OPTION = "database"; public static final String USER_OPTION = "user"; public static final String PASSWORD_ENV_OPTION = "passwordEnv"; private static final String DEFAULT_DATABASE = "bts63"; private static final String READ_ONLY_USER = "visitor"; /** 现场已知的全权限账号;写进交付配置就等于把误写风险带进现场。 */ private static final List FULL_PRIVILEGE_USERS = List.of("btsserver", "root"); /** 凭据文件相对进程工作目录定位,使开发(仓库根)与交付(应用目录)用同一个位置。 */ static final Path CREDENTIALS_FILE = LocalCredentials.DEFAULT_FILE; /** 返回本形态的适配器类型标识 {@code NEWARE_MYSQL}。 */ @Override public String adapterType() { return NewareMysqlAdapter.ADAPTER_TYPE; } /** 从实例配置取库名、只读账号与口令来源,装配单连接厂商数据源和只读适配器。 */ @Override public DeviceAdapter create(AdapterInstanceConfig config) { String database = option(config.sourceConfig(), DATABASE_OPTION, DEFAULT_DATABASE); String user = readOnlyUser(config.sourceConfig()); String key = option(config.sourceConfig(), PASSWORD_ENV_OPTION, NewareMysqlAdapter.DEFAULT_PASSWORD_ENV); String password = LocalCredentials.resolve(key, CREDENTIALS_FILE.toAbsolutePath(), System.getenv(), System::getProperty); SingleConnectionVendorDataSource source = new SingleConnectionVendorDataSource( jdbcUrl(config.host(), config.port(), database), user, password); return NewareMysqlAdapter.create(config, source); } /** * 解析厂商库账号:默认取现场取得的服务端只读账号,已知的全权限账号一律拒绝。 * *

准入只按账号名拦已知的全权限账号(那不是"写一次就回不去"的能力), * 真正的只读边界仍是服务端授权与语句白名单两层;这一层只是不把误配写进交付配置。 */ static String readOnlyUser(Map sourceConfig) { String user = option(sourceConfig, USER_OPTION, READ_ONLY_USER); if (FULL_PRIVILEGE_USERS.contains(user.toLowerCase(Locale.ROOT))) throw new DeviceAdapterException("厂商数据源只允许只读账号,拒绝全权限账号:" + user); return user; } /** * MySQL 5.5 那台不支持任何会话级只读机制,因此 URL 中不得出现 transaction_read_only * (ADR-0062);只读由服务端账号与语句白名单两层保证。 */ static String jdbcUrl(String host, int port, String database) { return "jdbc:mysql://" + host + ":" + port + "/" + database + "?connectTimeout=5000&socketTimeout=10000&useSSL=false&allowPublicKeyRetrieval=true" + "&zeroDateTimeBehavior=convertToNull&connectionTimeZone=Asia/Hong_Kong" + "&forceConnectionTimeZoneToSession=false"; } private static String option(Map sourceConfig, String key, String fallback) { String value = sourceConfig.get(key); return value == null || value.isBlank() ? fallback : value.trim(); } /** 供装配层声明该形态的运行时能力:可发现、可观测实时值、只读。 */ public static AdapterCapabilities capabilities() { return AdapterCapabilities.observeOnly(); } }