add restaurants check connection

This commit is contained in:
2026-04-24 00:55:20 +03:00
parent c47dad2af8
commit ff46a37956
11 changed files with 377 additions and 391 deletions

View File

@@ -22,6 +22,7 @@ import su.xserver.iikocon.handler.AdminHandler;
import su.xserver.iikocon.handler.AuthHandler;
import su.xserver.iikocon.handler.SecurityHandler;
import su.xserver.iikocon.handler.SetupHandler;
import su.xserver.iikocon.iiko.IikoOlapClient;
import su.xserver.iikocon.service.*;
import java.util.ArrayList;
@@ -350,6 +351,24 @@ public class MainVerticle extends AbstractVerticle {
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.get("/api/admin/restaurants/:id/check").handler(rc -> {
int id = Integer.parseInt(rc.pathParam("id"));
restaurantService.findById(id)
.onSuccess(rest -> {
if (rest == null) {
rc.response().setStatusCode(404).end();
} else {
IikoOlapClient iiko = new IikoOlapClient(vertx, rest);
iiko.checkConnection()
.onSuccess(res -> rc.response().putHeader("Content-Type", "application/json").end(res.encode()))
.onFailure(err -> rc.response().putHeader("Content-Type", "application/json").end(
new JsonObject().put("success", false).put("error", err.getMessage()).encode()));
}
})
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.post("/api/admin/restaurants").handler(rc -> {
JsonObject body = rc.body().asJsonObject();
String name = body.getString("name");

View File

@@ -0,0 +1,139 @@
package su.xserver.iikocon.iiko;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.WebClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
public class IikoOlapClient {
private static final Logger log = LoggerFactory.getLogger(IikoOlapClient.class);
private final WebClient webClient;
private final String iikoHost;
private final String iikoLogin;
private final String iikoPassHash;
public IikoOlapClient(Vertx vertx, String host, String login, String passHash, boolean https) {
this.webClient = WebClient.create(vertx);
this.iikoHost = (https ? "https://" : "http://") + host + (https ? ":443" : ":80");
this.iikoLogin = login;
this.iikoPassHash = passHash;
}
public IikoOlapClient(Vertx vertx, JsonObject rest) {
this.webClient = WebClient.create(vertx);
this.iikoHost = (rest.getBoolean("https") ? "https://" : "http://") + rest.getString("host") + (rest.getBoolean("https") ? ":443" : ":80");
this.iikoLogin = rest.getString("login");
this.iikoPassHash = rest.getString("password");
}
private Future<String> authenticate() {
Promise<String> promise = Promise.promise();
String url = iikoHost + "/resto/api/auth"; //?login=" + iikoLogin + "&pass=" + iikoPassHash;
webClient.getAbs(url)
.addQueryParam("login", iikoLogin)
.addQueryParam("pass", iikoPassHash)
.send()
.onSuccess(resp -> {
if (resp.statusCode() == 200) {
String token = resp.bodyAsString();
log.info("Authenticated, token: {}", token);
promise.complete(token);
} else {
promise.fail("Auth failed for " + iikoLogin + ": " + resp.statusCode());
}
})
.onFailure(promise::fail);
return promise.future();
}
private Future<Void> logout(String token) {
if (token == null || token.isEmpty()) {
return Future.succeededFuture();
}
Promise<Void> promise = Promise.promise();
String url = iikoHost + "/resto/api/logout";
webClient.getAbs(url)
.addQueryParam("key", token)
.send()
.onSuccess(resp -> {
// log.info("Logout completed for token, status {}", resp.statusCode());
log.info(resp.bodyAsString());
promise.complete();
})
.onFailure(err -> {
log.error("Logout request failed: {}", err.getMessage());
promise.complete();
});
return promise.future();
}
public Future<JsonObject> checkConnection() {
Promise<JsonObject> promise = Promise.promise();
long time = System.currentTimeMillis();
authenticate()
.onSuccess(token -> {
logout(token).mapEmpty();
promise.complete(new JsonObject()
.put("success", true)
.put("latency_ms", System.currentTimeMillis() - time)
);
})
.onFailure(promise::fail);
return promise.future();
}
public Future<JsonObject> handleGet(String uri, JsonObject params) {
Promise<JsonObject> promise = Promise.promise();
authenticate()
.onSuccess(token -> {
String url = appendQueryParams(iikoHost + uri, params.put("key", token));
log.info("Request to : {}", url);
webClient.getAbs(url)
.send()
.onSuccess(resp -> {
if (resp.statusCode() == 200) {
JsonObject body = resp.bodyAsJsonObject();
// Если есть обёртка data, распаковываем
JsonObject data = body.containsKey("data") && body.getValue("data") instanceof JsonObject
? body.getJsonObject("data")
: body;
logout(token).mapEmpty();
promise.complete(data);
} else {
promise.fail("Failed request to " + iikoHost + ": HTTP " + resp.statusCode());
}
})
.onFailure(promise::fail);
})
.onFailure(promise::fail);
return promise.future();
}
private String toQueryString(JsonObject params) {
if (params == null || params.isEmpty()) {
return "";
}
return "?" + params.stream()
.map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "=" +
URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
}
private String appendQueryParams(String url, JsonObject params) {
return url + toQueryString(params);
}
}

View File

@@ -1,50 +1,30 @@
package su.xserver.iikocon.test;
package su.xserver.iikocon.iiko;
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.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.mysqlclient.MySQLConnectOptions;
import io.vertx.sqlclient.*;
import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.PoolOptions;
import io.vertx.sqlclient.Tuple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
public class IikoOlapColumnsImporter {
private static final Logger log = LoggerFactory.getLogger(IikoOlapColumnsImporter.class);
private final WebClient httpClient;
private final Pool dbPool;
private final String iikoServer;
private final String iikoLogin;
private final String iikoPassword;
private static long time;
private final IikoOlapClient iikoOlapClient;
private static final List<String> REPORT_TYPES = List.of("SALES", "TRANSACTIONS", "DELIVERIES");
public IikoOlapColumnsImporter(Vertx vertx,
String iikoServer,
String iikoLogin,
String iikoPassword,
String dbHost, int dbPort,
String dbName, String dbUser, String dbPassword) {
WebClientOptions options = new WebClientOptions()
.setSsl(true)
.setTrustAll(true)
.setVerifyHost(false);
this.httpClient = WebClient.create(vertx, options);
this.iikoServer = iikoServer;
this.iikoLogin = iikoLogin;
this.iikoPassword = iikoPassword;
public IikoOlapColumnsImporter(Vertx vertx, String iikoServer, String iikoLogin, String iikoPassword, String dbHost, int dbPort, String dbName, String dbUser, String dbPassword) {
this.iikoOlapClient = new IikoOlapClient(vertx, iikoServer, iikoLogin, iikoPassword, true);
MySQLConnectOptions connectOptions = new MySQLConnectOptions()
.setHost(dbHost)
.setPort(dbPort)
@@ -56,7 +36,6 @@ public class IikoOlapColumnsImporter {
this.dbPool = Pool.pool(vertx, connectOptions, poolOptions);
}
// Главный метод: последовательно для каждого reportType делаем auth -> fetch -> store -> logout
public Future<Void> fetchAndStoreAll() {
return createTablesIfNotExist()
.compose(v -> processAllReportTypesSequentially())
@@ -65,7 +44,6 @@ public class IikoOlapColumnsImporter {
}
private Future<Void> processAllReportTypesSequentially() {
time = System.currentTimeMillis();
Future<Void> result = Future.succeededFuture();
for (String reportType : REPORT_TYPES) {
result = result.compose(v -> processOneReportType(reportType));
@@ -75,91 +53,19 @@ public class IikoOlapColumnsImporter {
private Future<Void> processOneReportType(String reportType) {
log.info("Processing report type: {}", reportType);
return authenticate()
.compose(token -> {
return fetchColumnsFromIiko(reportType, token)
.compose(columnsJson -> storeColumnsToDb(reportType, columnsJson))
.onComplete(ignored -> logout(token)); // logout всегда, даже при ошибке
});
}
// Аутентификация: GET /resto/api/auth?login=...&pass=SHA1
private Future<String> authenticate() {
Promise<String> promise = Promise.promise();
String passHash = sha1(iikoPassword);
String url = "https://" + iikoServer + ":443/resto/api/auth?login=" + iikoLogin + "&pass=" + passHash;
httpClient.getAbs(url)
.send()
.onSuccess(resp -> {
if (resp.statusCode() == 200) {
String token = resp.bodyAsString();
log.info("Authenticated, token: {}", token);
promise.complete(token);
} else {
promise.fail("Auth failed for " + iikoLogin + ": " + resp.statusCode());
}
})
.onFailure(promise::fail);
return promise.future();
}
// Logout: GET /resto/api/logout?key=токен
private Future<Void> logout(String token) {
if (token == null || token.isEmpty()) {
return Future.succeededFuture();
}
Promise<Void> promise = Promise.promise();
String url = "https://" + iikoServer + "/resto/api/logout?key=" + token;
httpClient.getAbs(url)
.send()
.onSuccess(resp -> {
log.info("Logout completed for token, status {}", resp.statusCode());
promise.complete();
})
.onFailure(err -> {
log.error("Logout request failed: {}", err.getMessage());
promise.complete(); // не ломаем цепочку
});
return promise.future();
return fetchColumnsFromIiko(reportType)
.compose(columnsJson -> storeColumnsToDb(reportType, columnsJson));
}
// Запрос полей для конкретного reportType
private Future<JsonObject> fetchColumnsFromIiko(String reportType, String token) {
private Future<JsonObject> fetchColumnsFromIiko(String reportType) {
Promise<JsonObject> promise = Promise.promise();
String url = "https://" + iikoServer + "/resto/api/v2/reports/olap/columns?key=" + token + "&reportType=" + reportType;
log.info("Connect to : {}", url);
httpClient.getAbs(url)
.send()
.onSuccess(resp -> {
if (resp.statusCode() == 200) {
JsonObject body = resp.bodyAsJsonObject();
// Если есть обёртка data, распаковываем
JsonObject data = body.containsKey("data") && body.getValue("data") instanceof JsonObject
? body.getJsonObject("data")
: body;
promise.complete(data);
log.info("time: {}", (System.currentTimeMillis() - time) + "ms");
} else {
promise.fail("Failed to fetch columns for " + reportType + ": HTTP " + resp.statusCode());
}
})
iikoOlapClient.handleGet("/resto/api/v2/reports/olap/columns", new JsonObject().put("reportType", reportType))
.onSuccess(promise::complete)
.onFailure(promise::fail);
return promise.future();
}
// SHA-1
private String sha1(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(input.getBytes());
return HexFormat.of().formatHex(digest).toLowerCase();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
return promise.future();
}
// ---------- Методы работы с БД (с префиксом iiko_) ----------
@@ -366,9 +272,4 @@ public class IikoOlapColumnsImporter {
default -> "string";
};
}
public void close() {
dbPool.close();
httpClient.close();
}
}

View File

@@ -1,4 +1,4 @@
package su.xserver.iikocon.test;
package su.xserver.iikocon.iiko;
import io.vertx.core.Vertx;
import org.slf4j.Logger;
@@ -16,7 +16,7 @@ public class Main {
vertx,
"folk-amber-co.iiko.it", // без https://
"4444",
"4444",
"92f2fd99879b0c2466ab8648afb63c49032379c1",
"phpmyadmin.xserver.su", // хост MariaDB
3306,
"test", // имя БД

View File

@@ -5,6 +5,7 @@ import io.vertx.core.json.JsonObject;
import io.vertx.ext.healthchecks.Status;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.healthchecks.HealthCheckHandler;
import su.xserver.iikocon.iiko.IikoOlapClient;
import java.util.Collections;
@@ -55,6 +56,20 @@ public class HealthCheckService {
.onFailure(err -> future.tryFail("DataBase ping failed: " + err.getMessage()));
});
// healthCheckHandler.register("iiko", future -> {
//
// IikoOlapClient iiko = new IikoOlapClient(vertx, "folk-amber-co.iiko.it", "4444", "92f2fd99879b0c2466ab8648afb63c49032379c1", true);
//
// iiko.checkConnection()
// .onSuccess(res -> {
// JsonObject data = new JsonObject()
// .put("name", "iiko")
// .put("latency_ms", res.getLong("latency_ms"));
// future.complete(Status.OK(data));
// })
// .onFailure(err -> future.tryFail("iiko ping failed: " + err.getMessage()));
// });
// Регистрируем endpoint /api/health
router.get("/api/health").handler(healthCheckHandler);
}

View File

@@ -6,11 +6,6 @@ import java.time.format.DateTimeParseException;
public class DateRangeSetup {
public static void main(String[] args) {
// Параметры по умолчанию
String login = "4444";
String password = "4444";
String server = "folk-amber-co.iiko.it";
String presetId = "7ddc40c3-9d5f-408f-aa1e-652964b36c6c";
// Вычисление dateFrom и dateTo
LocalDate today = LocalDate.now();
@@ -39,11 +34,6 @@ public class DateRangeSetup {
String formattedDateFrom = dateFrom.format(formatter);
String formattedDateTo = dateTo.format(formatter);
// Вывод переменных (можно заменить на дальнейшее использование)
System.out.println("login=" + login);
System.out.println("password=" + password);
System.out.println("server=" + server);
System.out.println("presetId=" + presetId);
System.out.println("dateFrom=" + formattedDateFrom);
System.out.println("dateTo=" + formattedDateTo);
}

View File

@@ -1,112 +0,0 @@
package su.xserver.iikocon.test;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
public class IikoOlapClient {
private final WebClient webClient;
private final String baseUrl;
private final String login;
private final String password;
// Конструктор клиента
public IikoOlapClient(Vertx vertx, String baseUrl, String login, String password) {
this.webClient = WebClient.create(vertx);
this.baseUrl = baseUrl;
this.login = login;
this.password = password;
}
// Основной метод для получения OLAP-отчета
public Future<JsonObject> getOlapReport(JsonObject reportRequest) {
Promise<JsonObject> promise = Promise.promise();
// 1. Аутентификация
authenticate()
.compose(this::getOrganizations) // 2. Получение организаций
.compose(orgId -> executeReport(reportRequest, orgId)) // 3. Запрос отчета
.onSuccess(promise::complete)
.onFailure(promise::fail);
return promise.future();
}
// Аутентификация и получение токена
private Future<String> authenticate() {
Promise<String> promise = Promise.promise();
JsonObject authRequest = new JsonObject()
.put("login", login)
.put("password", password);
webClient.post(443, baseUrl, "/resto/api/auth")
.ssl(true)
.putHeader("Content-Type", "application/json")
.sendJson(authRequest)
.onSuccess(response -> {
if (response.statusCode() == 200) {
String token = response.bodyAsJsonObject().getString("token");
promise.complete(token);
} else {
promise.fail("Authentication failed: " + response.statusMessage());
}
})
.onFailure(promise::fail);
return promise.future();
}
// Получение ID организации (для отчета)
private Future<String> getOrganizations(String token) {
Promise<String> promise = Promise.promise();
webClient.get(443, baseUrl, "/resto/api/organizations")
.ssl(true)
.putHeader("Authorization", "Bearer " + token)
.send()
.onSuccess(response -> {
if (response.statusCode() == 200) {
// Берем ID первой организации из списка
String orgId = response.bodyAsJsonArray()
.getJsonObject(0)
.getString("id");
promise.complete(orgId);
} else {
promise.fail("Failed to get organizations: " + response.statusMessage());
}
})
.onFailure(promise::fail);
return promise.future();
}
// Выполнение запроса OLAP-отчета
private Future<JsonObject> executeReport(JsonObject reportRequest, String organizationId) {
Promise<JsonObject> promise = Promise.promise();
// Добавляем ID организации в тело запроса
JsonObject fullRequest = reportRequest.copy()
.put("organizationId", organizationId);
webClient.post(443, baseUrl, "/resto/api/v2/reports/olap")
.ssl(true)
.putHeader("Content-Type", "application/json")
.sendJson(fullRequest)
.onSuccess(response -> {
if (response.statusCode() == 200) {
promise.complete(response.bodyAsJsonObject());
} else {
promise.fail("OLAP report request failed: " + response.statusMessage());
}
})
.onFailure(promise::fail);
return promise.future();
}
}

View File

@@ -1,109 +0,0 @@
package su.xserver.iikocon.test;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.ext.web.codec.BodyCodec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
public class ProxyVerticlev2 extends AbstractVerticle {
private static final Logger log = LoggerFactory.getLogger(ProxyVerticlev2.class);
private WebClient webClient;
@Override
public void start(Promise<Void> startPromise) {
webClient = WebClient.create(vertx, new WebClientOptions()
.setSsl(true)
.setTrustAll(true)
.setVerifyHost(false));
Router router = Router.router(vertx);
router.get("/").handler(this::handleGet);
int port = 80;
vertx.createHttpServer()
.requestHandler(router)
.listen(port).onComplete(http -> {
if (http.succeeded()) {
log.info("Proxy server started on port {}", port);
startPromise.complete();
} else {
startPromise.fail(http.cause());
}
});
}
private void handleGet(RoutingContext ctx) {
String server = "folk-amber-co.iiko.it";
String password = "4444";
String login = "4444";
String reportType = "DELIVERIES";
String signature = sha1(password);
String authUrl = "https://" + server + ":443/resto/api/auth?login=" + login + "&pass=" + signature;
webClient.getAbs(authUrl)
.as(BodyCodec.string())
.send()
.onSuccess(authResp -> {
if (authResp.statusCode() != 200) {
fail(ctx, authResp.statusCode(), "Authentication failed: " + authResp.statusMessage());
return;
}
String token = authResp.body();
String dataUrl = "https://" + server + "/resto/api/v2/reports/olap/columns" +
"?key=" + token + "&reportType=" + reportType;
log.info("URL: {}", dataUrl);
webClient.getAbs(dataUrl)
.as(BodyCodec.jsonObject())
.send()
.onSuccess(dataResp -> {
// logout (fire and forget)
webClient.getAbs("https://" + server + ":443/resto/api/logout?key=" + token)
.send()
.onFailure(err -> log.error("Logout failed: {}", err.getMessage()));
if (dataResp.statusCode() == 200) {
JsonObject responseBody = dataResp.body();
log.info(dataResp.headers().toString());
Object data = responseBody.getValue("data");
if (data == null) {
ctx.response().setStatusCode(200).end(responseBody.encode());
} else {
// data может быть массивом, объектом или другим типом
ctx.response().setStatusCode(200).end(Json.encode(data));
}
} else {
fail(ctx, dataResp.statusCode(), "External API error: " + dataResp.statusMessage());
}
})
.onFailure(err -> fail(ctx, 500, "Data request failed: " + err.getMessage()));
})
.onFailure(err -> fail(ctx, 500, "Auth request failed: " + err.getMessage()));
}
private String sha1(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(input.getBytes());
return HexFormat.of().formatHex(digest).toLowerCase();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private void fail(RoutingContext ctx, int status, String message) {
log.info("Error: {}", message);
ctx.response().setStatusCode(status).end(new JsonObject().put("error", message).encode());
}
}