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;
|
}
|
}
|