yeg
2026-09-28 b9edba7f696b2175cc74f111891d9051ce5e9c31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package com.battery.device.neware;
 
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.model.AdapterCapabilities;
import com.battery.device.api.registry.AdapterInstanceConfig;
import com.battery.device.api.registry.DeviceAdapterFactory;
 
/**
 * 按存储形态 {@code NEWARE_MYSQL} 构造直连厂商库的只读适配器实例。
 *
 * <p>连接口令只从环境变量或启动参数读取(变量名由实例配置的 {@code passwordEnv} 指定,
 * 缺省为 {@link NewareMysqlAdapter#DEFAULT_PASSWORD_ENV}),不进数据库也不进仓库。
 * 账号固定为现场取得的服务端只读账号,全权限账号不进入配置。
 */
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<String> FULL_PRIVILEGE_USERS = List.of("btsserver", "root");
 
    @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 password = credential(option(config.sourceConfig(), PASSWORD_ENV_OPTION,
                NewareMysqlAdapter.DEFAULT_PASSWORD_ENV));
        SingleConnectionVendorDataSource source = new SingleConnectionVendorDataSource(
                jdbcUrl(config.host(), config.port(), database), user, password);
        return NewareMysqlAdapter.create(config, source);
    }
 
    /**
     * 解析厂商库账号:默认取现场取得的服务端只读账号,已知的全权限账号一律拒绝。
     *
     * <p>准入只按账号名拦已知的全权限账号(那不是"写一次就回不去"的能力),
     * 真正的只读边界仍是服务端授权与语句白名单两层;这一层只是不把误配写进交付配置。
     */
    static String readOnlyUser(Map<String, String> 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;
    }
 
    /** 只读账号口令;启动参数优先于环境变量,便于本机排查而不必改环境。 */
    static String credential(String variableName)
    {
        if (variableName == null || variableName.isBlank())
            throw new DeviceAdapterException("未配置厂商库口令的取值变量名");
        String value = System.getenv(variableName);
        if (value == null || value.isBlank()) value = System.getProperty(variableName);
        if (value == null || value.isBlank())
            throw new DeviceAdapterException("缺少厂商库连接口令:请设置环境变量 " + variableName);
        return value;
    }
 
    /**
     * 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<String, String> 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();
    }
}