This commit is contained in:
2026-05-04 13:22:25 +03:00
parent f39d9ff11e
commit a61c527ef9
36 changed files with 794 additions and 29 deletions

View File

@@ -38,6 +38,7 @@ public class MainVerticle extends AbstractVerticle {
private UserService userService;
private RestaurantService restaurantService;
private ExternalDataBaseService externalDataBaseService;
private SettingsService settingsService;
@Override
@@ -64,6 +65,7 @@ public class MainVerticle extends AbstractVerticle {
userService = new UserService(db.getPool());
restaurantService = new RestaurantService(db.getPool());
settingsService = new SettingsService(db.getPool());
externalDataBaseService = new ExternalDataBaseService(db.getPool(), vertx);
userService.initDatabase().onFailure(err -> {
log.error("Failed to initialize database", err);
@@ -77,6 +79,10 @@ public class MainVerticle extends AbstractVerticle {
log.error("Failed to initialize database", err);
startPromise.fail(err);
});
externalDataBaseService.initDatabase().onFailure(err -> {
log.error("Failed to initialize database", err);
startPromise.fail(err);
});
createRouterAndStartHttp(startPromise);
@@ -418,6 +424,8 @@ public class MainVerticle extends AbstractVerticle {
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
externalDataBaseService.handleRoute(router);
new IikoHandler(vertx, router, db, restaurantService, authHandler);
return router;

View File

@@ -0,0 +1,259 @@
package su.xserver.iikocon.service;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.jdbcclient.JDBCConnectOptions;
import io.vertx.jdbcclient.JDBCPool;
import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.PoolOptions;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.templates.SqlTemplate;
import su.xserver.iikocon.handler.AdminHandler;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class ExternalDataBaseService {
private final Pool pool;
private final Vertx vertx;
public ExternalDataBaseService(Pool pool, Vertx vertx) {
this.pool = pool;
this.vertx = vertx;
}
public void handleRoute(Router router) {
router.get("/api/admin/database-connections").handler(rc -> this.getAllDataBases().onComplete(ar -> {
if (ar.succeeded()) {
rc.response()
.putHeader("Content-Type", "application/json")
.end(ar.result().encode());
} else {
rc.response().setStatusCode(500).end(ar.cause().getMessage());
}
}));
router.get("/api/admin/database-connections/:id/test").handler(AdminHandler::requireAdmin).handler(rc -> {
int id = Integer.parseInt(rc.pathParam("id"));
this.testConnection(id)
.onSuccess(result -> rc.response()
.setStatusCode(200)
.putHeader("Content-Type", "application/json")
.end(result.encode()))
.onFailure(err -> rc.response()
.setStatusCode(500)
.putHeader("Content-Type", "application/json")
.end(new JsonObject()
.put("success", false)
.put("error", err.getMessage())
.encode()));
});
router.post("/api/admin/database-connections").handler(AdminHandler::requireAdmin).handler(rc -> {
JsonObject body = rc.body().asJsonObject();
String name = body.getString("name");
String type = body.getString("type");
String host = body.getString("host");
int port = body.getInteger("port");
String database = body.getString("database");
String user = body.getString("user");
String password = body.getString("password");
if (name == null || type == null || host == null || port < 1 || database == null || user == null || password == null) {
rc.response().setStatusCode(400).end("Missing fields");
return;
}
this.createDataBase(name, type, host, port, database, user, password)
.onSuccess(v -> rc.response().setStatusCode(201).end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.put("/api/admin/database-connections/:id").handler(AdminHandler::requireAdmin).handler(rc -> {
int id = Integer.parseInt(rc.pathParam("id"));
JsonObject body = rc.body().asJsonObject();
String name = body.getString("name");
String type = body.getString("type");
String host = body.getString("host");
int port = body.getInteger("port");
String database = body.getString("database");
String user = body.getString("user");
String password = body.getString("password");
if (name == null || type == null || host == null || port < 1 || database == null || user == null) {
rc.response().setStatusCode(400).end("Missing fields");
return;
}
this.updateDataBase(id, name, type, host, port, database, user, password)
.onSuccess(v -> rc.response().end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.delete("/api/admin/database-connections/:id").handler(AdminHandler::requireAdmin).handler(rc -> {
int id = Integer.parseInt(rc.pathParam("id"));
this.deleteDataBase(id)
.onSuccess(v -> rc.response().end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
}
public Future<Void> initDatabase() {
String createTable = """
CREATE TABLE IF NOT EXISTS external_database (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL,
type VARCHAR(40) UNIQUE NOT NULL,
host VARCHAR(255) NOT NULL,
port INT NOT NULL,
database VARCHAR(255) NOT NULL,
user VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
""";
return pool.query(createTable).execute().mapEmpty();
}
public Future<Void> createDataBase(String name, String type, String host, int port, String database, String user, String password) {
Map<String, Object> params = Map.of(
"name", name,
"type", type,
"host", host,
"port", port,
"database", database,
"user", user,
"password", password
);
return SqlTemplate.forUpdate(pool,
"INSERT INTO external_database (name, type, host, port, database, user, password) VALUES (#{name}, #{type}, #{host}, #{port}, #{database}, #{user}, #{password})")
.execute(params)
.mapEmpty();
}
public Future<JsonArray> getAllDataBases() {
return pool.query("SELECT id, name, type, host, port, database, user, password, created, updated FROM external_database ORDER BY id")
.execute()
.map(rows -> {
JsonArray array = new JsonArray();
for (Row row : rows) {
array.add(new JsonObject()
.put("id", row.getInteger("id"))
.put("name", row.getString("name"))
.put("type", row.getString("type"))
.put("host", row.getString("host"))
.put("port", row.getInteger("port"))
.put("database", row.getString("database"))
.put("user", row.getString("user"))
.put("created", row.getLocalDateTime("created") != null ?
row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ?
row.getLocalDateTime("updated").toString() : null));
}
return array;
});
}
public Future<JsonObject> findById(int id) {
return SqlTemplate.forQuery(pool,
"SELECT id, name, type, host, port, database, user, password, created, updated FROM external_database WHERE id = #{id}")
.mapTo(row -> new JsonObject()
.put("id", row.getInteger("id"))
.put("name", row.getString("name"))
.put("type", row.getString("type"))
.put("host", row.getString("host"))
.put("port", row.getInteger("port"))
.put("database", row.getString("database"))
.put("user", row.getString("user"))
.put("password", row.getString("password"))
.put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null))
.execute(Collections.singletonMap("id", id))
.map(rows -> rows.iterator().hasNext() ? rows.iterator().next() : null);
}
public Future<Void> updateDataBase(int id, String name, String type, String host, int port, String database, String user, String password) {
Map<String, Object> params = new HashMap<>();
params.put("id", id);
params.put("name", name);
params.put("type", type);
params.put("host", host);
params.put("port", port);
params.put("database", database);
params.put("user", user);
String sql;
if (password != null && !password.isEmpty()) {
params.put("password", password);
sql = "UPDATE external_database SET name = #{name}, type = #{type}, host = #{host}, port = #{port}, database = #{database}, user = #{user}, password = #{password} WHERE id = #{id}";
} else {
sql = "UPDATE external_database SET name = #{name}, type = #{type}, host = #{host}, port = #{port}, database = #{database}, user = #{user} WHERE id = #{id}";
}
return SqlTemplate.forUpdate(pool, sql).execute(params).mapEmpty();
}
public Future<Void> deleteDataBase(int id) {
return SqlTemplate.forUpdate(pool, "DELETE FROM external_database WHERE id = #{id}")
.execute(Collections.singletonMap("id", id))
.mapEmpty();
}
public Future<JsonObject> testConnection(int id) {
Promise<JsonObject> promise = Promise.promise();
this.findById(id)
.onSuccess(conn -> {
String jdbcUrl = buildJdbcUrl(conn);
if (jdbcUrl == null) {
promise.fail("Unsupported database type: " + conn.getString("type"));
return;
}
JDBCConnectOptions connectOptions = new JDBCConnectOptions()
.setJdbcUrl(jdbcUrl)
.setDatabase(conn.getString("database"))
.setUser(conn.getString("user"))
.setPassword(conn.getString("password"));
PoolOptions poolOptions = new PoolOptions()
.setMaxSize(1);
Pool pool = JDBCPool.pool(vertx, connectOptions, poolOptions);
long startTime = System.currentTimeMillis();
pool
.query("SELECT 1")
.execute()
.onSuccess(rows -> {
long latency = System.currentTimeMillis() - startTime;
JsonObject result = new JsonObject()
.put("success", true)
.put("latency_ms", latency);
promise.complete(result);
pool.close();
})
.onFailure(err -> promise.fail("Connection failed: " + err.getMessage()));
})
.onFailure(promise::fail);
return promise.future();
}
private String buildJdbcUrl(JsonObject conn) {
return switch (conn.getString("type").toLowerCase()) {
case "mysql" -> String.format("jdbc:mysql://%s:%d",
conn.getString("host"), conn.getInteger("port"));
case "postgres" -> String.format("jdbc:postgresql://%s:%d",
conn.getString("host"), conn.getInteger("port"));
case "clickhouse" ->
String.format("jdbc:clickhouse://%s:%d",
conn.getString("host"), conn.getInteger("port"));
default -> null;
};
}
}

View File

@@ -0,0 +1,41 @@
package su.xserver.iikocon.test;
import io.vertx.core.Vertx;
import io.vertx.jdbcclient.JDBCConnectOptions;
import io.vertx.jdbcclient.JDBCPool;
import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.PoolOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ClickHouseJDBCExample {
private static final Logger log = LoggerFactory.getLogger(ClickHouseJDBCExample.class);
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
JDBCConnectOptions connectOptions = new JDBCConnectOptions()
.setJdbcUrl("jdbc:clickhouse://dl-import.aramagedec.ru:8123")
.setDatabase("test")
.setUser("clickhouse_admin")
.setPassword("7002ITinsta11");
PoolOptions poolOptions = new PoolOptions()
.setMaxSize(16);
Pool pool = JDBCPool.pool(vertx, connectOptions, poolOptions);
pool
.query("SELECT 1")
.execute()
.onSuccess(rows -> {
rows.forEach(row -> log.info(row.toJson().encodePrettily()));
vertx.close();
})
.onFailure(err -> {
log.error(err.getMessage());
vertx.close();
});
}
}