yeg
4 天以前 3194ad3ce5526148df9009aeaa917b888dc53b40
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
package com.battery.device.neware;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
 
/**
 * 可控的假厂商库:按新威 7.6 实测表结构({@code channel} / {@code test} / {@code data_*})回答适配器的查询,
 * 并把每一条到达"驱动"的语句原样记录下来。
 *
 * <p>它只回答适配器实际使用的查询形态,因此测试断言的是外部行为(发现清单、最新测试、分表定位、
 * 单位换算、水位过滤),不是 SQL 文本本身;记录下来的语句用于断言只读防线没有漏过非查询语句。
 */
final class FakeVendorDatabase
{
    record ChannelRow(long devUid, int unitId, int chlId) { }
 
    record TestRow(long devUid, int unitId, int chlId, long testId, String startTime,
            String firstTable, String secondTable) { }
 
    record SampleRow(String table, long testId, int unitId, int chlId, long seqId, int cycle, int stepId,
            String stepType, long testTimeMillis, int voltage, int current, int capacityCharge, int energyCharge,
            int capacityDischarge, int energyDischarge, String testAtime) { }
 
    /** 工步层一行:工步层不随采样分表滚动,所以没有表名。 */
    record CapStepRow(long testId, long devUid, int unitId, int chlId, long seqId, int cycle, int stepId,
            String stepType, long testTimeMillis, int voltage, int current, int capacityCharge, int energyCharge,
            int capacityDischarge, int energyDischarge, int middleVoltage, String testAtime) { }
 
    private final List<String> statements = new java.util.concurrent.CopyOnWriteArrayList<>();
    private final List<ChannelRow> channels = new ArrayList<>();
    private final List<TestRow> tests = new ArrayList<>();
    private final List<SampleRow> samples = new ArrayList<>();
    private final List<CapStepRow> capSteps = new ArrayList<>();
 
    FakeVendorDatabase registeredChannel(long devUid, int unitId, int chlId)
    {
        channels.add(new ChannelRow(devUid, unitId, chlId));
        return this;
    }
 
    FakeVendorDatabase test(TestRow row)
    {
        tests.add(row);
        return this;
    }
 
    FakeVendorDatabase sample(SampleRow row)
    {
        samples.add(row);
        return this;
    }
 
    FakeVendorDatabase capStep(CapStepRow row)
    {
        capSteps.add(row);
        return this;
    }
 
    /** 到达"驱动"的语句原文,按调用顺序。 */
    List<String> statements()
    {
        return List.copyOf(statements);
    }
 
    DataSource dataSource()
    {
        return new DataSource()
        {
            @Override
            public Connection getConnection()
            {
                return connection();
            }
 
            @Override
            public Connection getConnection(String username, String password)
            {
                return connection();
            }
 
            @Override
            public java.io.PrintWriter getLogWriter()
            {
                return null;
            }
 
            @Override
            public void setLogWriter(java.io.PrintWriter out)
            {
            }
 
            @Override
            public void setLoginTimeout(int seconds)
            {
            }
 
            @Override
            public int getLoginTimeout()
            {
                return 0;
            }
 
            @Override
            public java.util.logging.Logger getParentLogger()
            {
                return java.util.logging.Logger.getLogger("fake-vendor");
            }
 
            @Override
            public <T> T unwrap(Class<T> iface) throws SQLException
            {
                throw new SQLException("不支持解包");
            }
 
            @Override
            public boolean isWrapperFor(Class<?> iface)
            {
                return false;
            }
        };
    }
 
    private Connection connection()
    {
        return (Connection) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[] { Connection.class },
                (proxy, method, args) -> {
                    if ("prepareStatement".equals(method.getName())) return statement((String) args[0]);
                    if ("createStatement".equals(method.getName())) return plainStatement();
                    return defaultValue(method);
                });
    }
 
    /** 语句级执行:SQL 在执行时才出现,因此在这里才记录;探测类语句不回数据。 */
    private java.sql.Statement plainStatement()
    {
        return (java.sql.Statement) Proxy.newProxyInstance(getClass().getClassLoader(),
                new Class<?>[] { java.sql.Statement.class }, (proxy, method, args) -> {
                    if ("executeQuery".equals(method.getName()) || "execute".equals(method.getName())
                            || "executeUpdate".equals(method.getName()))
                    {
                        statements.add((String) args[0]);
                        return resultSet(List.of());
                    }
                    return defaultValue(method);
                });
    }
 
    private PreparedStatement statement(String sql)
    {
        statements.add(sql);
        List<Object> parameters = new ArrayList<>();
        return (PreparedStatement) Proxy.newProxyInstance(getClass().getClassLoader(),
                new Class<?>[] { PreparedStatement.class }, (proxy, method, args) -> {
                    switch (method.getName())
                    {
                        case "setObject", "setString", "setLong", "setInt" ->
                        {
                            int index = (Integer) args[0];
                            while (parameters.size() < index) parameters.add(null);
                            parameters.set(index - 1, args[1]);
                            return null;
                        }
                        case "executeQuery" -> { return resultSet(resolve(sql, parameters)); }
                        case "close" -> { return null; }
                        default -> { return defaultValue(method); }
                    }
                });
    }
 
    /** 按适配器使用的查询形态分派;参数顺序与适配器绑定顺序一致。 */
    private List<Map<String, Object>> resolve(String sql, List<Object> parameters)
    {
        String normalized = sql.toLowerCase().replaceAll("\\s+", " ");
        if (normalized.contains("from channel"))
            return channels.stream().sorted(Comparator.comparingLong(ChannelRow::devUid)
                    .thenComparingInt(ChannelRow::unitId).thenComparingInt(ChannelRow::chlId))
                    .map(row -> {
                        Map<String, Object> value = new LinkedHashMap<>();
                        value.put("dev_uid", row.devUid());
                        value.put("unit_id", row.unitId());
                        value.put("chl_id", row.chlId());
                        return value;
                    })
                    .toList();
        if (normalized.contains("from test") && normalized.contains("max(start_time)"))
            return latestTests(longValue(parameters, 0));
        if (normalized.contains("from test"))
            return latestTestForChannel(longValue(parameters, 0), intValue(parameters, 1), intValue(parameters, 2));
        if (normalized.contains("from cap_step"))
            return capStepRows(longValue(parameters, 0), longValue(parameters, 1), intValue(parameters, 2),
                    intValue(parameters, 3));
        if (normalized.contains("order by test_atime, seq_id limit"))
            return samplesAfter(tableOf(sql), parameters);
        return lastSampleOfTest(tableOf(sql), intValue(parameters, 0), intValue(parameters, 1),
                longValue(parameters, 2));
    }
 
    private List<Map<String, Object>> latestTests(long devUid)
    {
        Map<String, TestRow> latest = new LinkedHashMap<>();
        tests.stream().filter(row -> row.devUid() == devUid)
                .sorted(Comparator.comparing(TestRow::startTime).reversed().thenComparingLong(TestRow::testId))
                .forEach(row -> latest.putIfAbsent(row.unitId() + ":" + row.chlId(), row));
        return latest.values().stream().sorted(Comparator.comparingInt(TestRow::unitId)
                .thenComparingInt(TestRow::chlId)).map(this::testColumns).toList();
    }
 
    private List<Map<String, Object>> latestTestForChannel(long devUid, int unitId, int chlId)
    {
        return tests.stream().filter(row -> row.devUid() == devUid && row.unitId() == unitId && row.chlId() == chlId)
                .sorted(Comparator.comparing(TestRow::startTime).reversed().thenComparingLong(TestRow::testId))
                .limit(1).map(this::testColumns).toList();
    }
 
    private List<Map<String, Object>> lastSampleOfTest(String table, int unitId, int chlId, long testId)
    {
        return samples.stream().filter(row -> row.table().equals(table) && row.unitId() == unitId
                        && row.chlId() == chlId && row.testId() == testId)
                .sorted(Comparator.comparingLong(SampleRow::seqId).reversed()).limit(1)
                .map(this::sampleColumns).toList();
    }
 
    private List<Map<String, Object>> samplesAfter(String table, List<Object> parameters)
    {
        int unitId = intValue(parameters, 0), chlId = intValue(parameters, 1);
        // 没有带时间的下界时(水位表示界时来源里没有行)返回该通道的全部行。
        boolean bounded = parameters.size() >= 5;
        String watermark = bounded ? String.valueOf(parameters.get(2)) : null;
        long watermarkSeq = bounded ? longValue(parameters, 4) : 0L;
        return samples.stream().filter(row -> row.table().equals(table) && row.unitId() == unitId
                        && row.chlId() == chlId
                        && (!bounded || after(row.testAtime(), row.seqId(), watermark, watermarkSeq)))
                .sorted(Comparator.comparing(SampleRow::testAtime).thenComparingLong(SampleRow::seqId))
                .map(this::sampleColumns).toList();
    }
 
    private List<Map<String, Object>> capStepRows(long testId, long devUid, int unitId, int chlId)
    {
        return capSteps.stream().filter(row -> row.testId() == testId && row.devUid() == devUid
                        && row.unitId() == unitId && row.chlId() == chlId)
                .sorted(Comparator.comparingLong(CapStepRow::seqId)).map(this::capStepColumns).toList();
    }
 
    private Map<String, Object> capStepColumns(CapStepRow row)
    {
        Map<String, Object> value = new LinkedHashMap<>();
        value.put("seq_id", row.seqId());
        value.put("cycle", row.cycle());
        value.put("step_id", row.stepId());
        value.put("step_type", row.stepType());
        value.put("test_time", row.testTimeMillis());
        value.put("test_vol", row.voltage());
        value.put("test_cur", row.current());
        value.put("test_capchg", row.capacityCharge());
        value.put("test_engchg", row.energyCharge());
        value.put("test_capdchg", row.capacityDischarge());
        value.put("test_engdchg", row.energyDischarge());
        value.put("middle_vol", row.middleVoltage());
        value.put("test_atime", row.testAtime());
        return value;
    }
 
    private static boolean after(String atime, long seqId, String watermark, long watermarkSeq)
    {
        int time = atime.compareTo(watermark);
        return time > 0 || (time == 0 && seqId > watermarkSeq);
    }
 
    private static String tableOf(String sql)
    {
        java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("from\\s+`([A-Za-z0-9_]+)`")
                .matcher(sql.toLowerCase());
        if (!matcher.find()) throw new IllegalStateException("假厂商库无法识别分表名:" + sql);
        return matcher.group(1);
    }
 
    private Map<String, Object> testColumns(TestRow row)
    {
        Map<String, Object> value = new LinkedHashMap<>();
        value.put("unit_id", row.unitId());
        value.put("chl_id", row.chlId());
        value.put("test_id", row.testId());
        value.put("main_first_table", row.firstTable());
        value.put("main_second_table", row.secondTable());
        return value;
    }
 
    private Map<String, Object> sampleColumns(SampleRow row)
    {
        Map<String, Object> value = new LinkedHashMap<>();
        value.put("test_id", row.testId());
        value.put("seq_id", row.seqId());
        value.put("cycle", row.cycle());
        value.put("step_id", row.stepId());
        value.put("step_type", row.stepType());
        value.put("test_time", row.testTimeMillis());
        value.put("test_vol", row.voltage());
        value.put("test_cur", row.current());
        value.put("test_capchg", row.capacityCharge());
        value.put("test_engchg", row.energyCharge());
        value.put("test_capdchg", row.capacityDischarge());
        value.put("test_engdchg", row.energyDischarge());
        value.put("test_atime", row.testAtime());
        return value;
    }
 
    private static ResultSet resultSet(List<Map<String, Object>> rows)
    {
        List<String> labels = rows.isEmpty() ? List.of() : List.copyOf(rows.get(0).keySet());
        int[] cursor = { -1 };
        return (ResultSet) Proxy.newProxyInstance(FakeVendorDatabase.class.getClassLoader(),
                new Class<?>[] { ResultSet.class }, (proxy, method, args) -> {
                    switch (method.getName())
                    {
                        case "next" -> { cursor[0]++; return cursor[0] < rows.size(); }
                        case "close" -> { return null; }
                        case "getLong", "getInt", "getString", "getBigDecimal" ->
                        {
                            Object value = value(rows.get(cursor[0]), labels, args[0]);
                            if (value == null) return null;
                            return switch (method.getName())
                            {
                                case "getLong" -> ((Number) value).longValue();
                                case "getInt" -> ((Number) value).intValue();
                                case "getString" -> String.valueOf(value);
                                default -> value instanceof BigDecimal decimal ? decimal
                                        : BigDecimal.valueOf(((Number) value).longValue());
                            };
                        }
                        default -> { return defaultValue(method); }
                    }
                });
    }
 
    private static Object value(Map<String, Object> row, List<String> labels, Object key)
    {
        if (key instanceof Integer index) return row.get(labels.get(index - 1));
        return row.get(((String) key).toLowerCase());
    }
 
    private static int intValue(List<Object> parameters, int index)
    {
        return ((Number) parameters.get(index)).intValue();
    }
 
    private static long longValue(List<Object> parameters, int index)
    {
        return ((Number) parameters.get(index)).longValue();
    }
 
    private static Object defaultValue(Method method)
    {
        Class<?> type = method.getReturnType();
        if (!type.isPrimitive() || type == void.class) return null;
        if (type == boolean.class) return false;
        if (type == long.class) return 0L;
        if (type == double.class) return 0d;
        if (type == float.class) return 0f;
        if (type == char.class) return (char) 0;
        return 0;
    }
}