feat: productionize raw history ingestion
This commit is contained in:
@@ -1,70 +1,64 @@
|
||||
package com.lingniu.ingest.identity;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.lingniu.ingest.api.ProtocolId;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.lingniu.ingest.identity.config.VehicleIdentityProperties;
|
||||
import org.springframework.jdbc.core.ConnectionCallback;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.time.Duration;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.Map;
|
||||
|
||||
public final class MySqlVehicleIdentityService implements VehicleIdentityResolver, VehicleIdentityRegistry, AutoCloseable {
|
||||
public final class MySqlVehicleIdentityService implements VehicleIdentityResolver, VehicleIdentityRegistry {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MySqlVehicleIdentityService.class);
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper mapper;
|
||||
private final String tableName;
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final String table;
|
||||
private final String registrationTable;
|
||||
private final ScheduledExecutorService refresher;
|
||||
private volatile InMemoryVehicleIdentityService index;
|
||||
|
||||
public MySqlVehicleIdentityService(DataSource dataSource, String table) {
|
||||
this(dataSource, table, Duration.ZERO);
|
||||
}
|
||||
|
||||
public MySqlVehicleIdentityService(DataSource dataSource, String table, Duration refreshInterval) {
|
||||
if (dataSource == null) {
|
||||
throw new IllegalArgumentException("dataSource must not be null");
|
||||
public MySqlVehicleIdentityService(DataSource dataSource,
|
||||
VehicleIdentityProperties.Mysql properties,
|
||||
ObjectMapper mapper) {
|
||||
this.jdbc = new JdbcTemplate(dataSource);
|
||||
this.mapper = mapper == null ? new ObjectMapper() : mapper;
|
||||
VehicleIdentityProperties.Mysql mysql = properties == null ? new VehicleIdentityProperties.Mysql() : properties;
|
||||
this.tableName = safeTableName(mysql.getTableName());
|
||||
if (mysql.isInitializeSchema()) {
|
||||
initializeSchema();
|
||||
}
|
||||
this.dataSource = dataSource;
|
||||
this.table = sanitizeTable(table);
|
||||
this.registrationTable = this.table + "_registration";
|
||||
initializeSchema();
|
||||
this.index = loadIndex();
|
||||
this.refresher = startRefresher(refreshInterval);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(VehicleIdentityBinding binding) {
|
||||
if (binding == null || !binding.hasResolvedVin()) {
|
||||
return;
|
||||
}
|
||||
upsert(binding.plate(), binding.vin());
|
||||
index.bind(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(VehicleRegistrationBinding registration) {
|
||||
if (registration == null) {
|
||||
return;
|
||||
}
|
||||
upsertRegistration(registration);
|
||||
if (registration.hasResolvedVin()) {
|
||||
bind(new VehicleIdentityBinding(
|
||||
registration.protocol(),
|
||||
registration.vin(),
|
||||
registration.phone(),
|
||||
registration.deviceId(),
|
||||
registration.plate()));
|
||||
}
|
||||
String metadataJson = metadataJson(binding.metadata());
|
||||
jdbc.update("""
|
||||
INSERT INTO %s
|
||||
(protocol, vin, phone, device_id, plate, province, city, maker, device_type, plate_color, metadata_json,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
vin = VALUES(vin),
|
||||
device_id = VALUES(device_id),
|
||||
province = VALUES(province),
|
||||
city = VALUES(city),
|
||||
maker = VALUES(maker),
|
||||
device_type = VALUES(device_type),
|
||||
plate_color = VALUES(plate_color),
|
||||
metadata_json = VALUES(metadata_json),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""".formatted(tableName),
|
||||
protocol(binding.protocol()), binding.vin(), binding.phone(), binding.deviceId(), binding.plate(),
|
||||
binding.metadata().getOrDefault("province", ""),
|
||||
binding.metadata().getOrDefault("city", ""),
|
||||
binding.metadata().getOrDefault("maker", ""),
|
||||
binding.metadata().getOrDefault("deviceType", ""),
|
||||
binding.metadata().getOrDefault("plateColor", ""),
|
||||
metadataJson);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -72,223 +66,167 @@ public final class MySqlVehicleIdentityService implements VehicleIdentityResolve
|
||||
if (lookup == null) {
|
||||
return new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN);
|
||||
}
|
||||
return index.resolve(lookup);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
index = loadIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (refresher != null) {
|
||||
refresher.shutdownNow();
|
||||
if (!lookup.vin().isBlank()) {
|
||||
return new VehicleIdentity(lookup.vin(), true, VehicleIdentitySource.EXPLICIT_VIN);
|
||||
}
|
||||
VehicleIdentity byPhone = resolveBy("phone", lookup.protocol(), lookup.phone(), VehicleIdentitySource.BOUND_PHONE);
|
||||
if (byPhone != null) {
|
||||
return byPhone;
|
||||
}
|
||||
VehicleIdentity byDevice = resolveBy("device_id", lookup.protocol(), lookup.deviceId(), VehicleIdentitySource.BOUND_DEVICE_ID);
|
||||
if (byDevice != null) {
|
||||
return byDevice;
|
||||
}
|
||||
VehicleIdentity byPlate = resolveBy("plate", lookup.protocol(), lookup.plate(), VehicleIdentitySource.BOUND_PLATE);
|
||||
if (byPlate != null) {
|
||||
return byPlate;
|
||||
}
|
||||
if (!lookup.deviceId().isBlank()) {
|
||||
return new VehicleIdentity(lookup.deviceId(), false, VehicleIdentitySource.FALLBACK_DEVICE_ID);
|
||||
}
|
||||
if (!lookup.phone().isBlank()) {
|
||||
return new VehicleIdentity(lookup.phone(), false, VehicleIdentitySource.FALLBACK_PHONE);
|
||||
}
|
||||
if (!lookup.plate().isBlank()) {
|
||||
return new VehicleIdentity(lookup.plate(), false, VehicleIdentitySource.FALLBACK_PLATE);
|
||||
}
|
||||
return new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN);
|
||||
}
|
||||
|
||||
private VehicleIdentity resolveBy(String column, ProtocolId protocol, String externalId, VehicleIdentitySource source) {
|
||||
if (externalId == null || externalId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String safeColumn = switch (column) {
|
||||
case "phone", "device_id", "plate" -> column;
|
||||
default -> throw new IllegalArgumentException("unsupported identity lookup column: " + column);
|
||||
};
|
||||
List<VehicleIdentity> matches = jdbc.query("""
|
||||
SELECT vin
|
||||
FROM %s
|
||||
WHERE protocol = ? AND %s = ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
""".formatted(tableName, safeColumn),
|
||||
(rs, rowNum) -> identity(rs, source),
|
||||
protocol(protocol), externalId.trim());
|
||||
return matches.isEmpty() ? null : matches.getFirst();
|
||||
}
|
||||
|
||||
private VehicleIdentity identity(ResultSet rs, VehicleIdentitySource source) throws SQLException {
|
||||
return new VehicleIdentity(rs.getString("vin"), true, source);
|
||||
}
|
||||
|
||||
private void initializeSchema() {
|
||||
String sql = """
|
||||
jdbc.execute("""
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
plate VARCHAR(128) NOT NULL,
|
||||
vin VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY (plate),
|
||||
KEY idx_vehicle_identity_vin (vin)
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
protocol VARCHAR(32) NOT NULL,
|
||||
vin VARCHAR(32) NOT NULL,
|
||||
phone VARCHAR(64) NOT NULL DEFAULT '',
|
||||
device_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
plate VARCHAR(64) NOT NULL DEFAULT '',
|
||||
province VARCHAR(32) NOT NULL DEFAULT '',
|
||||
city VARCHAR(32) NOT NULL DEFAULT '',
|
||||
maker VARCHAR(64) NOT NULL DEFAULT '',
|
||||
device_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
plate_color VARCHAR(32) NOT NULL DEFAULT '',
|
||||
metadata_json TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_vehicle_identity_protocol_phone (protocol, phone),
|
||||
KEY idx_vehicle_identity_protocol_device (protocol, device_id),
|
||||
KEY idx_vehicle_identity_protocol_plate (protocol, plate)
|
||||
)
|
||||
""".formatted(table);
|
||||
String registrationSql = """
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
protocol VARCHAR(32) NOT NULL,
|
||||
phone VARCHAR(64) NOT NULL,
|
||||
vin VARCHAR(64) NOT NULL DEFAULT 'unknown',
|
||||
device_id VARCHAR(128) NOT NULL DEFAULT '',
|
||||
plate VARCHAR(128) NOT NULL DEFAULT '',
|
||||
province INT NULL,
|
||||
city INT NULL,
|
||||
maker VARCHAR(64) NOT NULL DEFAULT '',
|
||||
device_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
plate_color INT NULL,
|
||||
last_registered_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (protocol, phone),
|
||||
KEY idx_vehicle_registration_vin (vin),
|
||||
KEY idx_vehicle_registration_device (protocol, device_id),
|
||||
KEY idx_vehicle_registration_plate (protocol, plate)
|
||||
)
|
||||
""".formatted(registrationTable);
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute(sql);
|
||||
statement.execute(registrationSql);
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("vehicle identity mysql schema initialize failed: " + table, e);
|
||||
""".formatted(tableName));
|
||||
ensureColumn("phone", "phone VARCHAR(64) NOT NULL DEFAULT ''");
|
||||
ensureColumn("device_id", "device_id VARCHAR(64) NOT NULL DEFAULT ''");
|
||||
ensureColumn("plate", "plate VARCHAR(64) NOT NULL DEFAULT ''");
|
||||
ensureColumn("province", "province VARCHAR(32) NOT NULL DEFAULT ''");
|
||||
ensureColumn("city", "city VARCHAR(32) NOT NULL DEFAULT ''");
|
||||
ensureColumn("maker", "maker VARCHAR(64) NOT NULL DEFAULT ''");
|
||||
ensureColumn("device_type", "device_type VARCHAR(128) NOT NULL DEFAULT ''");
|
||||
ensureColumn("plate_color", "plate_color VARCHAR(32) NOT NULL DEFAULT ''");
|
||||
ensureColumn("metadata_json", "metadata_json TEXT");
|
||||
ensureColumn("created_at", "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
ensureColumn("updated_at", "updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP");
|
||||
ensureIndex("idx_vehicle_identity_protocol_device", "CREATE INDEX idx_vehicle_identity_protocol_device ON %s (protocol, device_id)");
|
||||
ensureIndex("idx_vehicle_identity_protocol_plate", "CREATE INDEX idx_vehicle_identity_protocol_plate ON %s (protocol, plate)");
|
||||
}
|
||||
|
||||
private void ensureColumn(String columnName, String definition) {
|
||||
if (!columnExists(columnName)) {
|
||||
jdbc.execute("ALTER TABLE " + tableName + " ADD COLUMN " + definition);
|
||||
}
|
||||
}
|
||||
|
||||
private InMemoryVehicleIdentityService loadIndex() {
|
||||
InMemoryVehicleIdentityService loaded = new InMemoryVehicleIdentityService();
|
||||
String sql = "SELECT plate, vin FROM " + table;
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql);
|
||||
ResultSet resultSet = statement.executeQuery()) {
|
||||
while (resultSet.next()) {
|
||||
bindGlobalPlate(loaded, resultSet.getString("vin"), resultSet.getString("plate"));
|
||||
private boolean columnExists(String columnName) {
|
||||
return Boolean.TRUE.equals(jdbc.execute((ConnectionCallback<Boolean>) connection -> {
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
for (String table : nameVariants(tableName)) {
|
||||
for (String column : nameVariants(columnName)) {
|
||||
try (ResultSet rs = metaData.getColumns(null, null, table, column)) {
|
||||
if (rs.next()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loadRegistrationIndex(loaded, connection);
|
||||
return loaded;
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("vehicle identity mysql index load failed: " + table, e);
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
|
||||
private void ensureIndex(String indexName, String ddlTemplate) {
|
||||
if (!indexExists(indexName)) {
|
||||
jdbc.execute(ddlTemplate.formatted(tableName));
|
||||
}
|
||||
}
|
||||
|
||||
private static void bindGlobalPlate(InMemoryVehicleIdentityService loaded, String vin, String plate) {
|
||||
for (ProtocolId protocol : new ProtocolId[]{
|
||||
ProtocolId.GB32960,
|
||||
ProtocolId.JT808,
|
||||
ProtocolId.MQTT_YUTONG,
|
||||
ProtocolId.XINDA_PUSH
|
||||
}) {
|
||||
loaded.bind(new VehicleIdentityBinding(protocol, vin, "", "", plate));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadRegistrationIndex(InMemoryVehicleIdentityService loaded, Connection connection) throws SQLException {
|
||||
String sql = "SELECT r.protocol, r.phone, r.device_id, r.plate, b.vin FROM " + registrationTable + " r "
|
||||
+ "JOIN " + table + " b ON b.plate = r.plate "
|
||||
+ "WHERE b.vin IS NOT NULL AND b.vin <> '' AND LOWER(b.vin) <> 'unknown'";
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql);
|
||||
ResultSet resultSet = statement.executeQuery()) {
|
||||
while (resultSet.next()) {
|
||||
loaded.bind(new VehicleIdentityBinding(
|
||||
protocol(resultSet.getString("protocol")),
|
||||
resultSet.getString("vin"),
|
||||
resultSet.getString("phone"),
|
||||
resultSet.getString("device_id"),
|
||||
resultSet.getString("plate")));
|
||||
private boolean indexExists(String indexName) {
|
||||
return Boolean.TRUE.equals(jdbc.execute((ConnectionCallback<Boolean>) connection -> {
|
||||
DatabaseMetaData metaData = connection.getMetaData();
|
||||
for (String table : nameVariants(tableName)) {
|
||||
try (ResultSet rs = metaData.getIndexInfo(null, null, table, false, false)) {
|
||||
while (rs.next()) {
|
||||
String existing = rs.getString("INDEX_NAME");
|
||||
if (existing != null && existing.equalsIgnoreCase(indexName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loadLegacyRegistrationVinIndex(loaded, connection);
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
|
||||
private void loadLegacyRegistrationVinIndex(InMemoryVehicleIdentityService loaded, Connection connection) throws SQLException {
|
||||
String sql = "SELECT protocol, phone, device_id, plate, vin FROM " + registrationTable
|
||||
+ " WHERE vin IS NOT NULL AND vin <> '' AND LOWER(vin) <> 'unknown'";
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql);
|
||||
ResultSet resultSet = statement.executeQuery()) {
|
||||
while (resultSet.next()) {
|
||||
loaded.bind(new VehicleIdentityBinding(
|
||||
protocol(resultSet.getString("protocol")),
|
||||
resultSet.getString("vin"),
|
||||
resultSet.getString("phone"),
|
||||
resultSet.getString("device_id"),
|
||||
resultSet.getString("plate")));
|
||||
}
|
||||
}
|
||||
private static String[] nameVariants(String name) {
|
||||
return new String[]{
|
||||
name,
|
||||
name.toLowerCase(Locale.ROOT),
|
||||
name.toUpperCase(Locale.ROOT)
|
||||
};
|
||||
}
|
||||
|
||||
private ScheduledExecutorService startRefresher(Duration refreshInterval) {
|
||||
if (refreshInterval == null || refreshInterval.isZero() || refreshInterval.isNegative()) {
|
||||
return null;
|
||||
private String metadataJson(Map<String, String> metadata) {
|
||||
if (metadata == null || metadata.isEmpty()) {
|
||||
return "{}";
|
||||
}
|
||||
long delayMillis = Math.max(refreshInterval.toMillis(), 1000L);
|
||||
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "vehicle-identity-mysql-refresh");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
executor.scheduleWithFixedDelay(this::safeRefresh, delayMillis, delayMillis, TimeUnit.MILLISECONDS);
|
||||
return executor;
|
||||
}
|
||||
|
||||
private void safeRefresh() {
|
||||
try {
|
||||
refresh();
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("vehicle identity mysql refresh failed table={}", table, e);
|
||||
return mapper.writeValueAsString(metadata);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("vehicle identity metadata serialize failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void upsert(String plate, String vin) {
|
||||
String normalizedPlate = normalize(plate);
|
||||
if (normalizedPlate.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String sql = "INSERT INTO " + table + " (plate, vin) "
|
||||
+ "VALUES (?, ?) "
|
||||
+ "ON DUPLICATE KEY UPDATE vin = VALUES(vin)";
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, normalizedPlate);
|
||||
statement.setString(2, vin.trim());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("vehicle identity mysql bind failed: " + table, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void upsertRegistration(VehicleRegistrationBinding registration) {
|
||||
String phone = normalize(registration.phone());
|
||||
if (phone.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String vin = registration.hasResolvedVin() ? registration.vin().trim() : "unknown";
|
||||
String sql = "INSERT INTO " + registrationTable
|
||||
+ " (protocol, phone, vin, device_id, plate, province, city, maker, device_type, plate_color) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
+ "ON DUPLICATE KEY UPDATE "
|
||||
+ "vin = CASE WHEN VALUES(vin) <> 'unknown' THEN VALUES(vin) ELSE vin END, "
|
||||
+ "device_id = VALUES(device_id), plate = VALUES(plate), province = VALUES(province), "
|
||||
+ "city = VALUES(city), maker = VALUES(maker), device_type = VALUES(device_type), "
|
||||
+ "plate_color = VALUES(plate_color), last_registered_at = CURRENT_TIMESTAMP";
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, protocolName(registration.protocol()));
|
||||
statement.setString(2, phone);
|
||||
statement.setString(3, vin);
|
||||
statement.setString(4, normalize(registration.deviceId()));
|
||||
statement.setString(5, normalize(registration.plate()));
|
||||
setInteger(statement, 6, registration.province());
|
||||
setInteger(statement, 7, registration.city());
|
||||
statement.setString(8, normalize(registration.maker()));
|
||||
statement.setString(9, normalize(registration.deviceType()));
|
||||
setInteger(statement, 10, registration.plateColor());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("vehicle identity mysql registration bind failed: " + registrationTable, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setInteger(PreparedStatement statement, int index, Integer value) throws SQLException {
|
||||
if (value == null) {
|
||||
statement.setObject(index, null);
|
||||
} else {
|
||||
statement.setInt(index, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static String sanitizeTable(String table) {
|
||||
String value = table == null || table.isBlank() ? "vehicle_identity_binding" : table.trim();
|
||||
if (!value.matches("[A-Za-z0-9_]+")) {
|
||||
throw new IllegalArgumentException("vehicle identity mysql table must contain only letters, digits, and underscore");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String protocolName(ProtocolId protocol) {
|
||||
private static String protocol(ProtocolId protocol) {
|
||||
return protocol == null ? "UNKNOWN" : protocol.name();
|
||||
}
|
||||
|
||||
private static ProtocolId protocol(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return ProtocolId.UNKNOWN;
|
||||
private static String safeTableName(String value) {
|
||||
String name = value == null || value.isBlank() ? "vehicle_identity_bindings" : value.trim();
|
||||
if (!name.matches("[A-Za-z0-9_]+")) {
|
||||
throw new IllegalArgumentException("invalid vehicle identity mysql table name: " + value);
|
||||
}
|
||||
try {
|
||||
return ProtocolId.valueOf(value.trim().toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return ProtocolId.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||
return name.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,26 @@ package com.lingniu.ingest.identity;
|
||||
|
||||
import com.lingniu.ingest.api.ProtocolId;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public record VehicleIdentityBinding(
|
||||
ProtocolId protocol,
|
||||
String vin,
|
||||
String phone,
|
||||
String deviceId,
|
||||
String plate
|
||||
String plate,
|
||||
Map<String, String> metadata
|
||||
) {
|
||||
public VehicleIdentityBinding(ProtocolId protocol, String vin, String phone, String deviceId, String plate) {
|
||||
this(protocol, vin, phone, deviceId, plate, Map.of());
|
||||
}
|
||||
|
||||
public VehicleIdentityBinding {
|
||||
vin = normalize(vin);
|
||||
phone = normalize(phone);
|
||||
deviceId = normalize(deviceId);
|
||||
plate = normalize(plate);
|
||||
metadata = metadata == null ? Map.of() : Map.copyOf(metadata);
|
||||
if (vin.isBlank()) {
|
||||
throw new IllegalArgumentException("vin must not be blank");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.lingniu.ingest.identity;
|
||||
|
||||
public enum VehicleIdentitySource {
|
||||
EXPLICIT_VIN,
|
||||
REGISTERED,
|
||||
BOUND_PHONE,
|
||||
BOUND_DEVICE_ID,
|
||||
BOUND_PLATE,
|
||||
|
||||
@@ -11,15 +11,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.SQLFeatureNotSupportedException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(VehicleIdentityProperties.class)
|
||||
@@ -40,22 +35,15 @@ public class VehicleIdentityAutoConfiguration {
|
||||
return new FileVehicleIdentityService(Path.of(properties.getFile().getPath()), objectMapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(DataSource.class)
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "mysql")
|
||||
public DataSource vehicleIdentityDataSource(VehicleIdentityProperties properties) {
|
||||
VehicleIdentityProperties.Mysql mysql = properties.getMysql();
|
||||
return new DriverManagerVehicleIdentityDataSource(
|
||||
mysql.getDriverClassName(), mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean({VehicleIdentityResolver.class, VehicleIdentityRegistry.class})
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "mysql")
|
||||
public MySqlVehicleIdentityService mySqlVehicleIdentityService(VehicleIdentityProperties properties,
|
||||
DataSource dataSource) {
|
||||
public MySqlVehicleIdentityService mysqlVehicleIdentityService(VehicleIdentityProperties properties,
|
||||
ObjectMapper objectMapper) {
|
||||
VehicleIdentityProperties.Mysql mysql = properties.getMysql();
|
||||
return new MySqlVehicleIdentityService(dataSource, mysql.getTable(), mysql.getRefreshInterval());
|
||||
DataSource dataSource = new DriverManagerDataSource(
|
||||
mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword());
|
||||
return new MySqlVehicleIdentityService(dataSource, mysql, objectMapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -65,61 +53,4 @@ public class VehicleIdentityAutoConfiguration {
|
||||
public InMemoryVehicleIdentityService vehicleIdentityService() {
|
||||
return new InMemoryVehicleIdentityService();
|
||||
}
|
||||
|
||||
private static final class DriverManagerVehicleIdentityDataSource implements DataSource {
|
||||
private final String jdbcUrl;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private PrintWriter logWriter;
|
||||
private int loginTimeout;
|
||||
|
||||
private DriverManagerVehicleIdentityDataSource(String driverClassName,
|
||||
String jdbcUrl,
|
||||
String username,
|
||||
String password) {
|
||||
if (jdbcUrl == null || jdbcUrl.isBlank()) {
|
||||
throw new IllegalArgumentException("lingniu.ingest.identity.mysql.jdbc-url is required");
|
||||
}
|
||||
if (driverClassName != null && !driverClassName.isBlank()) {
|
||||
try {
|
||||
Class.forName(driverClassName.trim());
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IllegalArgumentException("vehicle identity mysql driver class not found: "
|
||||
+ driverClassName, e);
|
||||
}
|
||||
}
|
||||
this.jdbcUrl = jdbcUrl;
|
||||
this.username = username == null ? "" : username;
|
||||
this.password = password == null ? "" : password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
DriverManager.setLoginTimeout(loginTimeout);
|
||||
return DriverManager.getConnection(jdbcUrl, username, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection(String username, String password) throws SQLException {
|
||||
DriverManager.setLoginTimeout(loginTimeout);
|
||||
return DriverManager.getConnection(jdbcUrl, username, password);
|
||||
}
|
||||
|
||||
@Override public PrintWriter getLogWriter() { return logWriter; }
|
||||
@Override public void setLogWriter(PrintWriter out) { this.logWriter = out; }
|
||||
@Override public void setLoginTimeout(int seconds) { this.loginTimeout = seconds; }
|
||||
@Override public int getLoginTimeout() { return loginTimeout; }
|
||||
@Override public Logger getParentLogger() throws SQLFeatureNotSupportedException {
|
||||
throw new SQLFeatureNotSupportedException();
|
||||
}
|
||||
@Override public <T> T unwrap(Class<T> iface) throws SQLException {
|
||||
if (iface.isInstance(this)) {
|
||||
return iface.cast(this);
|
||||
}
|
||||
throw new SQLException("not a wrapper for " + iface);
|
||||
}
|
||||
@Override public boolean isWrapperFor(Class<?> iface) {
|
||||
return iface.isInstance(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package com.lingniu.ingest.identity.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@ConfigurationProperties(prefix = "lingniu.ingest.identity")
|
||||
public class VehicleIdentityProperties {
|
||||
|
||||
@@ -26,24 +24,21 @@ public class VehicleIdentityProperties {
|
||||
}
|
||||
|
||||
public static class Mysql {
|
||||
private String table = "vehicle_identity_binding";
|
||||
private String jdbcUrl = "";
|
||||
private String username = "";
|
||||
private String jdbcUrl = "jdbc:mysql://127.0.0.1:3306/lingniu_vehicle?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai";
|
||||
private String username = "root";
|
||||
private String password = "";
|
||||
private String driverClassName = "com.mysql.cj.jdbc.Driver";
|
||||
private Duration refreshInterval = Duration.ofSeconds(60);
|
||||
private String tableName = "vehicle_identity_bindings";
|
||||
private boolean initializeSchema = true;
|
||||
|
||||
public String getTable() { return table; }
|
||||
public void setTable(String table) { this.table = table; }
|
||||
public String getJdbcUrl() { return jdbcUrl; }
|
||||
public void setJdbcUrl(String jdbcUrl) { this.jdbcUrl = jdbcUrl; }
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
public String getDriverClassName() { return driverClassName; }
|
||||
public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; }
|
||||
public Duration getRefreshInterval() { return refreshInterval; }
|
||||
public void setRefreshInterval(Duration refreshInterval) { this.refreshInterval = refreshInterval; }
|
||||
public String getTableName() { return tableName; }
|
||||
public void setTableName(String tableName) { this.tableName = tableName; }
|
||||
public boolean isInitializeSchema() { return initializeSchema; }
|
||||
public void setInitializeSchema(boolean initializeSchema) { this.initializeSchema = initializeSchema; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user