From c8017837799818bd11bd075b7ac0419b17cf7142 Mon Sep 17 00:00:00 2001 From: Danil-Bodry Date: Fri, 1 May 2026 19:04:18 +0300 Subject: [PATCH] up, add OLAP columns page --- frontend/src/components/Layout/AppLayout.vue | 17 + frontend/src/locales/en.json | 43 +- frontend/src/locales/ru.json | 45 +- frontend/src/router/index.ts | 66 +- frontend/src/views/OlapColumnsView.vue | 641 ++++++++++++++++++ .../java/su/xserver/iikocon/MainVerticle.java | 3 + .../su/xserver/iikocon/iiko/IikoHandler.java | 263 +++++++ .../xserver/iikocon/iiko/IikoOlapClient.java | 10 +- .../iikocon/iiko/IikoOlapColumnsImporter.java | 245 +++---- .../java/su/xserver/iikocon/iiko/Main.java | 43 -- .../xserver/iikocon/test/DateRangeSetup.java | 21 +- .../xserver/iikocon/test/ProxyVerticle.java | 183 ----- 12 files changed, 1145 insertions(+), 435 deletions(-) create mode 100644 frontend/src/views/OlapColumnsView.vue create mode 100644 src/main/java/su/xserver/iikocon/iiko/IikoHandler.java delete mode 100644 src/main/java/su/xserver/iikocon/iiko/Main.java delete mode 100644 src/main/java/su/xserver/iikocon/test/ProxyVerticle.java diff --git a/frontend/src/components/Layout/AppLayout.vue b/frontend/src/components/Layout/AppLayout.vue index 0e2151a..e66c7e3 100644 --- a/frontend/src/components/Layout/AppLayout.vue +++ b/frontend/src/components/Layout/AppLayout.vue @@ -76,6 +76,23 @@ {{ t('app.restaurants') }} + + + + + + {{ t('app.olapColumns') }} + + + +
+

{{ t('olap.columnsTitle') }}

+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+ + + + +

{{ t('app.loading') }}

+
+
+ {{ t('olap.noColumnsFound') }} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ t('olap.fieldKey') }}{{ t('common.name') }}{{ t('olap.reportTypes') }}{{ t('olap.type') }}{{ t('olap.tags') }}{{ t('olap.aggregation') }}{{ t('olap.grouping') }}{{ t('olap.filtering') }}{{ t('common.actions') }}
{{ col.fieldKey }}{{ col.name }} +
+ + {{ rt }} + +
+
+ + {{ col.typeNormal || col.type }} + + +
+ + {{ tag }} + + +
+
+ + + + + + + + + +
+ + + + + + +
+
+
+
+ + + +
+
+
+
+ +
+

{{ initModalTitle }}

+ +
+ + +
+
+ + + + +
+
+ + +
+
+
+
+ + + +
+
+

{{ rest.name }}

+

{{ rest.host }}

+
+
+
+ + + +
+
+
+ {{ t('olap.noRestaurantsFound') }} +
+
+ + +
+ + +
+
+
+
+
+ + + +
+
+
+
+
+
+ + + +
+

{{ t('olap.refreshWarningTitle') }}

+

+ {{ t('olap.refreshWarningMessage', { restaurant: pendingRestaurantName }) }} +

+

{{ t('olap.refreshWarningConfirm') }}

+
+ + +
+
+
+
+
+
+ + + +
+
+
+
+
+

{{ t('olap.editField') }}

+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+ + + +
+
+
+
+
+
+ + + +
+

{{ t('olap.deleteField') }}

+

{{ t('olap.deleteFieldConfirm') }}

+
+ + +
+
+
+
+
+
+ + +
+
+ + + + +

+ {{ initializingText }} +

+

+ {{ t('olap.waitMessage') }} +

+
+
+
+
+ + + + + diff --git a/src/main/java/su/xserver/iikocon/MainVerticle.java b/src/main/java/su/xserver/iikocon/MainVerticle.java index 4ddc0e5..cbc64d8 100644 --- a/src/main/java/su/xserver/iikocon/MainVerticle.java +++ b/src/main/java/su/xserver/iikocon/MainVerticle.java @@ -19,6 +19,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import su.xserver.iikocon.config.AppConfig; import su.xserver.iikocon.handler.*; +import su.xserver.iikocon.iiko.IikoHandler; import su.xserver.iikocon.iiko.IikoOlapClient; import su.xserver.iikocon.service.*; @@ -417,6 +418,8 @@ public class MainVerticle extends AbstractVerticle { .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); }); + new IikoHandler(vertx, router, db, restaurantService); + return router; } diff --git a/src/main/java/su/xserver/iikocon/iiko/IikoHandler.java b/src/main/java/su/xserver/iikocon/iiko/IikoHandler.java new file mode 100644 index 0000000..d078c3f --- /dev/null +++ b/src/main/java/su/xserver/iikocon/iiko/IikoHandler.java @@ -0,0 +1,263 @@ +package su.xserver.iikocon.iiko; + +import io.vertx.core.Future; +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.ext.web.RoutingContext; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.Tuple; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import su.xserver.iikocon.service.DataBaseService; +import su.xserver.iikocon.service.RestaurantService; + +public class IikoHandler { + + private final Logger log = LoggerFactory.getLogger("[IikoHandler]"); + private final DataBaseService db; + private final Vertx vertx; + private final RestaurantService restaurantService; + + public IikoHandler(Vertx vertx, Router router, DataBaseService db, RestaurantService restaurantService) { + this.vertx = vertx; + this.restaurantService = restaurantService; + this.db = db; + + createTablesIfNotExist().onFailure(err -> { + log.error("Failed to initialize database", err); + }); + + router.get("/api/reports/olap/columns").handler(this::getColumns); + router.delete("/api/reports/olap/columns/:fieldKey").handler(this::deleteColumn); + router.post("/api/reports/olap/initialize").handler(this::postInitialize); + } + + private void getColumns(RoutingContext ctx) { + getAllFieldsWithReportAndTags() + .onSuccess(ar -> ctx.response() + .putHeader("Content-Type", "application/json") + .end(ar.encodePrettily())) + .onFailure(err -> ctx.response() + .setStatusCode(500) + .end(err.getMessage())); + } + + public void deleteColumn(RoutingContext ctx) { + String fieldKey = ctx.pathParam("fieldKey"); + String sql = "DELETE FROM iiko_fields_common WHERE field_key = ?"; + + db.getPool().preparedQuery(sql) + .execute(Tuple.of(fieldKey)) + .onSuccess(res -> { + ctx.end(); + }) + .onFailure(err -> ctx.response().setStatusCode(500).end(err.getMessage())); + } + + private void postInitialize(RoutingContext ctx) { + JsonObject body = ctx.body().asJsonObject(); + + if (body == null) { + ctx.response() + .setStatusCode(400) + .end("Request body is missing or not a JSON object"); + return; + } + + if (!body.containsKey("restaurantId") || body.getValue("restaurantId") == null) { + ctx.response() + .setStatusCode(400) + .end("restaurantId is required"); + return; + } + + Integer restaurantId; + try { + restaurantId = body.getInteger("restaurantId"); + if (restaurantId == null) { + throw new IllegalArgumentException("restaurantId must be a number"); + } + } catch (ClassCastException e) { + ctx.response() + .setStatusCode(400) + .end("restaurantId must be a valid integer"); + return; + } + + restaurantService.findById(restaurantId) + .onSuccess(rest -> { + + IikoOlapClient iiko = new IikoOlapClient(vertx, rest); + + iiko.checkConnection() + .onSuccess(ping -> clearTables() + .onSuccess(data -> { + IikoOlapColumnsImporter importer = new IikoOlapColumnsImporter(iiko, db); + + importer.fetchAndStoreAll() + .onSuccess(res -> ctx.end("OK")) + .onFailure(err -> ctx.response() + .setStatusCode(400) + .end(err.getMessage())); + }) + .onFailure(err -> ctx.response() + .setStatusCode(400) + .end(err.getMessage()))) + .onFailure(err -> ctx.response().setStatusCode(400).end(err.getMessage())); + }) + .onFailure(err -> ctx.response() + .setStatusCode(400) + .end(err.getMessage())); + } + + public Future getAllFieldsWithReportAndTags() { + String sql = """ + SELECT + fc.field_key, + fc.field_key_normal, + fc.name, + fc.type, + fc.type_normal, + fc.aggregation_allowed, + fc.grouping_allowed, + fc.filtering_allowed, + GROUP_CONCAT(DISTINCT rt.name ORDER BY rt.name SEPARATOR ',') AS report_names, + GROUP_CONCAT(DISTINCT t.tag_name ORDER BY t.tag_name SEPARATOR ',') AS tag_names + FROM iiko_fields_common fc + LEFT JOIN iiko_report_type_fields rtf ON fc.field_id = rtf.field_id + LEFT JOIN iiko_report_types rt ON rtf.report_type_id = rt.report_type_id + LEFT JOIN iiko_field_tags ft ON fc.field_id = ft.field_id + LEFT JOIN iiko_tags t ON ft.tag_id = t.tag_id + GROUP BY fc.field_id + ORDER BY fc.field_key + """; + + return db.getPool().query(sql).execute() + .map(rows -> { + JsonArray columnsArray = new JsonArray(); + for (Row row : rows) { + + String reportNamesStr = row.getString("report_names"); + JsonArray reportTypes = new JsonArray(); + if (reportNamesStr != null && !reportNamesStr.isBlank()) { + for (String name : reportNamesStr.split(",")) { + reportTypes.add(name.trim()); + } + } + + String tagNamesStr = row.getString("tag_names"); + JsonArray tags = new JsonArray(); + if (tagNamesStr != null && !tagNamesStr.isBlank()) { + for (String tag : tagNamesStr.split(",")) { + tags.add(tag.trim()); + } + } + + JsonObject fieldObj = new JsonObject() + .put("fieldKey", row.getString("field_key")) + .put("fieldKeyNormal", row.getString("field_key_normal")) + .put("reportTypes", reportTypes) + .put("name", row.getString("name")) + .put("type", row.getString("type")) + .put("typeNormal", row.getString("type_normal")) + .put("aggregationAllowed", row.getBoolean("aggregation_allowed")) + .put("groupingAllowed", row.getBoolean("grouping_allowed")) + .put("filteringAllowed", row.getBoolean("filtering_allowed")) + .put("tags", tags); + + columnsArray.add(fieldObj); + } + return new JsonObject().put("columns", columnsArray); + }); + } + + private Future createTablesIfNotExist() { + String createReportTypes = """ + CREATE TABLE IF NOT EXISTS iiko_report_types ( + report_type_id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL, + description TEXT NOT NULL + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + """; + + String createFieldsCommon = """ + CREATE TABLE IF NOT EXISTS iiko_fields_common ( + field_id INT AUTO_INCREMENT PRIMARY KEY, + field_key VARCHAR(255) NOT NULL UNIQUE, + field_key_normal VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + type_normal VARCHAR(50) NOT NULL, + aggregation_allowed BOOLEAN NOT NULL DEFAULT 0, + grouping_allowed BOOLEAN NOT NULL DEFAULT 0, + filtering_allowed BOOLEAN NOT NULL DEFAULT 0 + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + """; + + String createReportTypeFields = """ + CREATE TABLE IF NOT EXISTS iiko_report_type_fields ( + report_type_id INT NOT NULL, + field_id INT NOT NULL, + PRIMARY KEY (report_type_id, field_id), + FOREIGN KEY (report_type_id) REFERENCES iiko_report_types(report_type_id) ON DELETE CASCADE, + FOREIGN KEY (field_id) REFERENCES iiko_fields_common(field_id) ON DELETE CASCADE + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + """; + + String createTags = """ + CREATE TABLE IF NOT EXISTS iiko_tags ( + tag_id INT AUTO_INCREMENT PRIMARY KEY, + tag_name VARCHAR(100) UNIQUE NOT NULL + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + """; + + String createFieldTags = """ + CREATE TABLE IF NOT EXISTS iiko_field_tags ( + field_id INT NOT NULL, + tag_id INT NOT NULL, + PRIMARY KEY (field_id, tag_id), + FOREIGN KEY (field_id) REFERENCES iiko_fields_common(field_id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES iiko_tags(tag_id) ON DELETE CASCADE + ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci + """; + + String idxKeyNormal = "CREATE INDEX IF NOT EXISTS idx_fields_common_key_normal ON iiko_fields_common(field_key_normal)"; + String idxFieldName = "CREATE INDEX IF NOT EXISTS idx_fields_common_name ON iiko_fields_common(name)"; + String idxFieldTagsTag = "CREATE INDEX IF NOT EXISTS idx_field_tags_tag_id ON iiko_field_tags(tag_id)"; + + return db.getPool().query(createReportTypes).execute() + .compose(v -> db.getPool().query(createFieldsCommon).execute()) + .compose(v -> db.getPool().query(createReportTypeFields).execute()) + .compose(v -> db.getPool().query(createTags).execute()) + .compose(v -> db.getPool().query(createFieldTags).execute()) + .compose(v -> db.getPool().query(idxKeyNormal).execute()) + .compose(v -> db.getPool().query(idxFieldName).execute()) + .compose(v -> db.getPool().query(idxFieldTagsTag).execute()) + .mapEmpty(); + } + + private Future clearTables() { + String sql = """ + -- Отключаем проверку внешних ключей + SET FOREIGN_KEY_CHECKS = 0; + + -- Удаляем данные из всех таблиц (порядок не важен при отключённой проверке) + DELETE FROM iiko_field_tags; + DELETE FROM iiko_report_type_fields; + DELETE FROM iiko_fields_common; + DELETE FROM iiko_tags; + DELETE FROM iiko_report_types; + + -- Сбрасываем счётчики AUTO_INCREMENT (чтобы новые ID начинались с 1) + ALTER TABLE iiko_fields_common AUTO_INCREMENT = 1; + ALTER TABLE iiko_tags AUTO_INCREMENT = 1; + ALTER TABLE iiko_report_types AUTO_INCREMENT = 1; + + -- Включаем проверку обратно + SET FOREIGN_KEY_CHECKS = 1; + """; + return db.getPool().query(sql).execute().mapEmpty(); + } +} diff --git a/src/main/java/su/xserver/iikocon/iiko/IikoOlapClient.java b/src/main/java/su/xserver/iikocon/iiko/IikoOlapClient.java index ec0d8b3..66a59a3 100644 --- a/src/main/java/su/xserver/iikocon/iiko/IikoOlapClient.java +++ b/src/main/java/su/xserver/iikocon/iiko/IikoOlapClient.java @@ -20,13 +20,6 @@ public class IikoOlapClient { 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"); @@ -36,7 +29,7 @@ public class IikoOlapClient { private Future authenticate() { Promise promise = Promise.promise(); - String url = iikoHost + "/resto/api/auth"; //?login=" + iikoLogin + "&pass=" + iikoPassHash; + String url = iikoHost + "/resto/api/auth"; webClient.getAbs(url) .addQueryParam("login", iikoLogin) @@ -104,7 +97,6 @@ public class IikoOlapClient { .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; diff --git a/src/main/java/su/xserver/iikocon/iiko/IikoOlapColumnsImporter.java b/src/main/java/su/xserver/iikocon/iiko/IikoOlapColumnsImporter.java index 13c1fc6..8a4bed1 100644 --- a/src/main/java/su/xserver/iikocon/iiko/IikoOlapColumnsImporter.java +++ b/src/main/java/su/xserver/iikocon/iiko/IikoOlapColumnsImporter.java @@ -1,16 +1,12 @@ 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.mysqlclient.MySQLConnectOptions; -import io.vertx.sqlclient.Pool; -import io.vertx.sqlclient.PoolOptions; import io.vertx.sqlclient.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import su.xserver.iikocon.service.DataBaseService; import java.util.ArrayList; import java.util.List; @@ -18,27 +14,17 @@ import java.util.List; public class IikoOlapColumnsImporter { private static final Logger log = LoggerFactory.getLogger("[IikoOlapColumnsImporter]"); - private final Pool dbPool; + private final DataBaseService db; private final IikoOlapClient iikoOlapClient; - private static final List 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) { - this.iikoOlapClient = new IikoOlapClient(vertx, iikoServer, iikoLogin, iikoPassword, true); - MySQLConnectOptions connectOptions = new MySQLConnectOptions() - .setHost(dbHost) - .setPort(dbPort) - .setDatabase(dbName) - .setUser(dbUser) - .setPassword(dbPassword) - .setCharset("utf8mb4"); - PoolOptions poolOptions = new PoolOptions().setMaxSize(5); - this.dbPool = Pool.pool(vertx, connectOptions, poolOptions); + public IikoOlapColumnsImporter(IikoOlapClient iikoOlapClient, DataBaseService db) { + this.iikoOlapClient = iikoOlapClient; + this.db = db; } public Future fetchAndStoreAll() { - return createTablesIfNotExist() - .compose(v -> processAllReportTypesSequentially()) + return processAllReportTypesSequentially() .onSuccess(v -> log.info("All reports imported successfully")) .onFailure(err -> log.error("Import failed: {}", err.getMessage())); } @@ -57,18 +43,11 @@ public class IikoOlapColumnsImporter { .compose(columnsJson -> storeColumnsToDb(reportType, columnsJson)); } - // Запрос полей для конкретного reportType private Future fetchColumnsFromIiko(String reportType) { - Promise promise = Promise.promise(); - - iikoOlapClient.handleGet("/resto/api/v2/reports/olap/columns", new JsonObject().put("reportType", reportType)) - .onSuccess(promise::complete) - .onFailure(promise::fail); - - return promise.future(); + return iikoOlapClient.handleGet("/resto/api/v2/reports/olap/columns", + new JsonObject().put("reportType", reportType)); } - // ---------- Методы работы с БД (с префиксом iiko_) ---------- private Future storeColumnsToDb(String reportType, JsonObject columns) { return getOrCreateReportType(reportType) .compose(reportTypeId -> { @@ -82,86 +61,86 @@ public class IikoOlapColumnsImporter { } private Future getOrCreateReportType(String reportType) { - Promise promise = Promise.promise(); String selectSql = "SELECT report_type_id FROM iiko_report_types WHERE name = ?"; - dbPool.preparedQuery(selectSql) - .execute(Tuple.of(reportType)) - .onComplete(ar -> { - if (ar.succeeded() && ar.result().size() > 0) { - promise.complete(ar.result().iterator().next().getInteger("report_type_id")); - } else if (ar.succeeded()) { - String insertSql = "INSERT INTO iiko_report_types (name, description) VALUES (?, ?)"; - dbPool.preparedQuery(insertSql) - .execute(Tuple.of(reportType, "OLAP report type: " + reportType)) - .onComplete(insAr -> { - if (insAr.succeeded()) { - dbPool.preparedQuery(selectSql) - .execute(Tuple.of(reportType)) - .onComplete(selAr -> { - if (selAr.succeeded() && selAr.result().size() > 0) { - promise.complete(selAr.result().iterator().next().getInteger("report_type_id")); - } else { - promise.fail("Cannot retrieve inserted report_type_id for " + reportType); - } - }); - } else { - promise.fail(insAr.cause()); - } - }); + return db.getPool().preparedQuery(selectSql).execute(Tuple.of(reportType)) + .compose(rows -> { + if (rows.size() > 0) { + return Future.succeededFuture(rows.iterator().next().getInteger("report_type_id")); } else { - promise.fail(ar.cause()); + String insertSql = "INSERT INTO iiko_report_types (name, description) VALUES (?, ?)"; + return db.getPool().preparedQuery(insertSql) + .execute(Tuple.of(reportType, "OLAP report type: " + reportType)) + .compose(ignored -> + db.getPool().preparedQuery(selectSql).execute(Tuple.of(reportType)) + .map(rows2 -> rows2.iterator().next().getInteger("report_type_id")) + ); } }); - return promise.future(); } + /** + * Сохранить одно поле (без дублирования). + * Сначала получаем/создаём запись в iiko_fields_common, + * затем связываем её с report_type_id через iiko_report_type_fields, + * потом обрабатываем теги. + */ private Future storeSingleField(int reportTypeId, String fieldKey, JsonObject fieldDef) { - // Нормализованный ключ (без точек) String fieldKeyNormal = fieldKey.replace('.', '_'); - String name = fieldDef.getString("name"); String originalType = fieldDef.getString("type"); String typeNormal = normalizeType(originalType); - boolean aggregationAllowed = fieldDef.getBoolean("aggregationAllowed", false); boolean groupingAllowed = fieldDef.getBoolean("groupingAllowed", false); boolean filteringAllowed = fieldDef.getBoolean("filteringAllowed", false); JsonArray tagsArray = fieldDef.getJsonArray("tags", new JsonArray()); - String insertFieldSql = """ - INSERT INTO iiko_fields ( - report_type_id, field_key, field_key_normal, name, type, type_normal, - aggregation_allowed, grouping_allowed, filtering_allowed - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - field_key_normal = VALUES(field_key_normal), - name = VALUES(name), - type_normal = VALUES(type_normal), - aggregation_allowed = VALUES(aggregation_allowed), - grouping_allowed = VALUES(grouping_allowed), - filtering_allowed = VALUES(filtering_allowed) - """; + return getOrCreateCommonField(fieldKey, fieldKeyNormal, name, originalType, typeNormal, + aggregationAllowed, groupingAllowed, filteringAllowed) + .compose(fieldId -> linkFieldToReportType(reportTypeId, fieldId) + .compose(v -> processTags(fieldId, tagsArray)) + ); + } - return dbPool.preparedQuery(insertFieldSql) - .execute(Tuple.of( - reportTypeId, fieldKey, fieldKeyNormal, name, originalType, typeNormal, - aggregationAllowed, groupingAllowed, filteringAllowed - )) - .compose(ignored -> { - String selectFieldIdSql = "SELECT field_id FROM iiko_fields WHERE report_type_id = ? AND field_key = ?"; - return dbPool.preparedQuery(selectFieldIdSql) - .execute(Tuple.of(reportTypeId, fieldKey)) - .compose(rows -> { - if (rows.size() == 0) { - return Future.failedFuture("Field not found after upsert: " + fieldKey); - } - int fieldId = rows.iterator().next().getInteger("field_id"); - return processTags(fieldId, tagsArray); - }); + /** + * Найти или создать поле в iiko_fields_common (по уникальному field_key). + */ + private Future getOrCreateCommonField(String fieldKey, String fieldKeyNormal, String name, + String type, String typeNormal, + boolean aggAllowed, boolean groupAllowed, boolean filterAllowed) { + String selectSql = "SELECT field_id FROM iiko_fields_common WHERE field_key = ?"; + return db.getPool().preparedQuery(selectSql).execute(Tuple.of(fieldKey)) + .compose(rows -> { + if (rows.size() > 0) { + return Future.succeededFuture(rows.iterator().next().getInteger("field_id")); + } else { + String insertSql = """ + INSERT INTO iiko_fields_common + (field_key, field_key_normal, name, type, type_normal, + aggregation_allowed, grouping_allowed, filtering_allowed) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """; + return db.getPool().preparedQuery(insertSql) + .execute(Tuple.of(fieldKey, fieldKeyNormal, name, type, typeNormal, + aggAllowed, groupAllowed, filterAllowed)) + .compose(ignored -> + db.getPool().preparedQuery(selectSql).execute(Tuple.of(fieldKey)) + .map(rows2 -> rows2.iterator().next().getInteger("field_id")) + ); + } }); } + /** + * Привязать поле к типу отчёта (если ещё не привязано). + */ + private Future linkFieldToReportType(int reportTypeId, int fieldId) { + String sql = "INSERT IGNORE INTO iiko_report_type_fields (report_type_id, field_id) VALUES (?, ?)"; + return db.getPool().preparedQuery(sql).execute(Tuple.of(reportTypeId, fieldId)).mapEmpty(); + } + + /** + * Обработать теги поля (теги одинаковы для всех типов отчётов). + */ private Future processTags(int fieldId, JsonArray tags) { List> tagFutures = new ArrayList<>(); for (Object tagObj : tags) { @@ -173,93 +152,25 @@ public class IikoOlapColumnsImporter { } private Future getOrCreateTag(String tagName) { - Promise promise = Promise.promise(); String selectSql = "SELECT tag_id FROM iiko_tags WHERE tag_name = ?"; - dbPool.preparedQuery(selectSql) - .execute(Tuple.of(tagName)) - .onComplete(ar -> { - if (ar.succeeded() && ar.result().size() > 0) { - promise.complete(ar.result().iterator().next().getInteger("tag_id")); + return db.getPool().preparedQuery(selectSql).execute(Tuple.of(tagName)) + .compose(rows -> { + if (rows.size() > 0) { + return Future.succeededFuture(rows.iterator().next().getInteger("tag_id")); } else { String insertSql = "INSERT IGNORE INTO iiko_tags (tag_name) VALUES (?)"; - dbPool.preparedQuery(insertSql) - .execute(Tuple.of(tagName)) - .onComplete(insAr -> { - // После IGNORE всё равно выбираем ID (он мог уже существовать) - dbPool.preparedQuery(selectSql) - .execute(Tuple.of(tagName)) - .onComplete(selAr -> { - if (selAr.succeeded() && selAr.result().size() > 0) { - promise.complete(selAr.result().iterator().next().getInteger("tag_id")); - } else { - promise.fail("Cannot retrieve tag_id for " + tagName); - } - }); - }); + return db.getPool().preparedQuery(insertSql).execute(Tuple.of(tagName)) + .compose(ignored -> + db.getPool().preparedQuery(selectSql).execute(Tuple.of(tagName)) + .map(rows2 -> rows2.iterator().next().getInteger("tag_id")) + ); } }); - return promise.future(); } private Future linkFieldTag(int fieldId, int tagId) { String sql = "INSERT IGNORE INTO iiko_field_tags (field_id, tag_id) VALUES (?, ?)"; - return dbPool.preparedQuery(sql) - .execute(Tuple.of(fieldId, tagId)) - .mapEmpty(); - } - - private Future createTablesIfNotExist() { - String createReportTypesTable = """ - CREATE TABLE IF NOT EXISTS iiko_report_types ( - report_type_id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(50) UNIQUE NOT NULL, - description TEXT NOT NULL - ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci - """; - String createFieldsTable = """ - CREATE TABLE IF NOT EXISTS iiko_fields ( - field_id INT AUTO_INCREMENT PRIMARY KEY, - report_type_id INT NOT NULL, - field_key VARCHAR(255) NOT NULL, - field_key_normal VARCHAR(255) NOT NULL, - name VARCHAR(255) NOT NULL, - type VARCHAR(50) NOT NULL, - type_normal VARCHAR(50) NOT NULL, - aggregation_allowed BOOLEAN NOT NULL DEFAULT 0, - grouping_allowed BOOLEAN NOT NULL DEFAULT 0, - filtering_allowed BOOLEAN NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uk_fields_report_type_field_key (report_type_id, field_key), - FOREIGN KEY (report_type_id) REFERENCES iiko_report_types(report_type_id) ON DELETE RESTRICT - ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci - """; - String createTagsTable = """ - CREATE TABLE IF NOT EXISTS iiko_tags ( - tag_id INT AUTO_INCREMENT PRIMARY KEY, - tag_name VARCHAR(100) UNIQUE NOT NULL - ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci - """; - String createFieldTagsTable = """ - CREATE TABLE IF NOT EXISTS iiko_field_tags ( - field_id INT NOT NULL, - tag_id INT NOT NULL, - PRIMARY KEY (field_id, tag_id), - FOREIGN KEY (field_id) REFERENCES iiko_fields(field_id) ON DELETE CASCADE, - FOREIGN KEY (tag_id) REFERENCES iiko_tags(tag_id) ON DELETE CASCADE - ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci - """; - String createIdxFieldsReportType = "CREATE INDEX IF NOT EXISTS idx_fields_report_type ON iiko_fields(report_type_id)"; - String createIdxFieldsName = "CREATE INDEX IF NOT EXISTS idx_fields_name ON iiko_fields(name)"; - String createIdxFieldTagsTagId = "CREATE INDEX IF NOT EXISTS idx_field_tags_tag_id ON iiko_field_tags(tag_id)"; - - return dbPool.query(createReportTypesTable).execute() - .compose(ignored -> dbPool.query(createFieldsTable).execute()) - .compose(ignored -> dbPool.query(createTagsTable).execute()) - .compose(ignored -> dbPool.query(createFieldTagsTable).execute()) - .compose(ignored -> dbPool.query(createIdxFieldsReportType).execute()) - .compose(ignored -> dbPool.query(createIdxFieldsName).execute()) - .compose(ignored -> dbPool.query(createIdxFieldTagsTagId).execute()) - .mapEmpty(); + return db.getPool().preparedQuery(sql).execute(Tuple.of(fieldId, tagId)).mapEmpty(); } private String normalizeType(String iikoType) { diff --git a/src/main/java/su/xserver/iikocon/iiko/Main.java b/src/main/java/su/xserver/iikocon/iiko/Main.java deleted file mode 100644 index fa33f2c..0000000 --- a/src/main/java/su/xserver/iikocon/iiko/Main.java +++ /dev/null @@ -1,43 +0,0 @@ -package su.xserver.iikocon.iiko; - -import io.vertx.core.Vertx; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Main { - private static final Logger log = LoggerFactory.getLogger(Main.class); - - public static void main(String[] args) { - long time = System.currentTimeMillis(); - - Vertx vertx = Vertx.vertx(); - - IikoOlapColumnsImporter importer = new IikoOlapColumnsImporter( - vertx, - "folk-amber-co.iiko.it", // без https:// - "4444", - "92f2fd99879b0c2466ab8648afb63c49032379c1", - "phpmyadmin.xserver.su", // хост MariaDB - 3306, - "test", // имя БД - "test", - "test" - ); - - - - importer.fetchAndStoreAll() - .onComplete(ar -> { - if (ar.succeeded()) { - System.out.println("Import completed successfully."); - log.info("time to sc: {}", (System.currentTimeMillis() - time) + "ms"); - - } else { - System.err.println("Import failed: " + ar.cause().getMessage()); - } -// importer.close(); -// vertx.close(); - }); - - } -} diff --git a/src/main/java/su/xserver/iikocon/test/DateRangeSetup.java b/src/main/java/su/xserver/iikocon/test/DateRangeSetup.java index 97e43a3..a687f6d 100644 --- a/src/main/java/su/xserver/iikocon/test/DateRangeSetup.java +++ b/src/main/java/su/xserver/iikocon/test/DateRangeSetup.java @@ -2,7 +2,6 @@ package su.xserver.iikocon.test; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; public class DateRangeSetup { public static void main(String[] args) { @@ -10,29 +9,11 @@ public class DateRangeSetup { // Вычисление dateFrom и dateTo LocalDate today = LocalDate.now(); LocalDate dateFrom = today.minusDays(7); - LocalDate dateTo = today; - - // Переопределение из аргументов командной строки - if (args.length > 0 && args[0] != null && !args[0].isEmpty()) { - try { - dateFrom = LocalDate.parse(args[0]); - } catch (DateTimeParseException e) { - System.err.println("Ошибка парсинга dateFrom: " + args[0] + ". Используется значение по умолчанию."); - } - } - - if (args.length > 1 && args[1] != null && !args[1].isEmpty()) { - try { - dateTo = LocalDate.parse(args[1]); - } catch (DateTimeParseException e) { - System.err.println("Ошибка парсинга dateTo: " + args[1] + ". Используется значение по умолчанию."); - } - } // Форматирование дат в YYYY-MM-DD DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); String formattedDateFrom = dateFrom.format(formatter); - String formattedDateTo = dateTo.format(formatter); + String formattedDateTo = today.format(formatter); System.out.println("dateFrom=" + formattedDateFrom); System.out.println("dateTo=" + formattedDateTo); diff --git a/src/main/java/su/xserver/iikocon/test/ProxyVerticle.java b/src/main/java/su/xserver/iikocon/test/ProxyVerticle.java deleted file mode 100644 index 839bd1a..0000000 --- a/src/main/java/su/xserver/iikocon/test/ProxyVerticle.java +++ /dev/null @@ -1,183 +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 java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.HexFormat; - -public class ProxyVerticle extends AbstractVerticle { - - private WebClient webClient; - - @Override - public void start(Promise startPromise) { - webClient = WebClient.create(vertx, new WebClientOptions() - .setSsl(true) - .setTrustAll(true) - .setVerifyHost(false)); - - Router router = Router.router(vertx); - router.post("/api/proxy").handler(this::handlePost); - router.get("/api/proxy").handler(this::handleGet); - - int port = 8080; - vertx.createHttpServer() - .requestHandler(router) - .listen(port).onComplete(http -> { - if (http.succeeded()) { - System.out.println("Proxy server started on port " + port); - startPromise.complete(); - } else { - startPromise.fail(http.cause()); - } - }); - } - - private void handlePost(RoutingContext ctx) { - String apiServer = System.getenv("IIKO_API_SERVER"); - String apiLogin = System.getenv("IIKO_API_LOGIN"); - String apiPass = System.getenv("IIKO_API_PASS"); - String externalEndpoint = System.getenv("IIKO_API_ENDPOINT"); - if (externalEndpoint == null || externalEndpoint.isBlank()) { - externalEndpoint = "/your-endpoint"; - } - - if (apiServer == null || apiLogin == null || apiPass == null) { - fail(ctx, 500, "Missing required environment variables: IIKO_API_SERVER, IIKO_API_LOGIN, IIKO_API_PASS"); - return; - } - - JsonObject body = ctx.body().asJsonObject(); - if (body == null) { - fail(ctx, 400, "Request body must be JSON"); - return; - } - - String signature = sha1(apiPass); - String authUrl = "https://" + apiServer + ":443/resto/api/auth?login=" + apiLogin + "&pass=" + signature; - String finalExternalEndpoint = externalEndpoint; - 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 targetUrl = "https://" + apiServer + finalExternalEndpoint; - webClient.request(HttpMethod.POST, targetUrl) - .putHeader("Content-Type", "application/json") - .as(BodyCodec.jsonObject()) - .sendJsonObject(body) - .onSuccess(apiResp -> { - webClient.getAbs("https://" + apiServer + ":443/resto/api/logout?key=" + token) - .send() - .onFailure(err -> System.err.println("Logout failed: " + err.getMessage())); - if (apiResp.statusCode() == 200) { - ctx.response().setStatusCode(200).end(apiResp.body().encode()); - } else { - fail(ctx, apiResp.statusCode(), "External API error: " + apiResp.statusMessage()); - } - }) - .onFailure(err -> fail(ctx, 500, "Request to external API failed: " + err.getMessage())); - }) - .onFailure(err -> fail(ctx, 500, "Auth request failed: " + err.getMessage())); - } - - private void handleGet(RoutingContext ctx) { - String presetId = ctx.queryParam("presetId").stream().findFirst().orElse(null); - String dateFrom = ctx.queryParam("dateFrom").stream().findFirst().orElse(null); - String dateTo = ctx.queryParam("dateTo").stream().findFirst().orElse(null); - String server = ctx.queryParam("server").stream().findFirst().orElse(null); - String password = ctx.queryParam("password").stream().findFirst().orElse(null); - String login = ctx.queryParam("login").stream().findFirst().orElse(null); - String type = ctx.queryParam("type").stream().findFirst().orElse(null); - String rootType = ctx.queryParam("rootType").stream().findFirst().orElse(null); - - if (server == null || login == null || password == null) { - fail(ctx, 400, "Missing required parameters: server, login, password"); - return; - } - - 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; - if ("entity".equals(type)) { - dataUrl = "https://" + server + "/resto/api/v2/entities/list?key=" + token; - if (rootType != null && !rootType.isBlank()) { - dataUrl += "&rootType=" + rootType; - } - } else { - if (presetId == null || dateFrom == null || dateTo == null) { - fail(ctx, 400, "Missing presetId, dateFrom or dateTo for report request"); - return; - } - dataUrl = "https://" + server + "/resto/api/v2/reports/olap/byPresetId/" + presetId + - "?key=" + token + "&dateFrom=" + dateFrom + "&dateTo=" + dateTo; - } - System.out.println("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 -> System.err.println("Logout failed: " + err.getMessage())); - if (dataResp.statusCode() == 200) { - JsonObject responseBody = dataResp.body(); - if ("entity".equals(type)) { - ctx.response().setStatusCode(200).end(responseBody.encode()); - } else { - 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) { - System.err.println("Error: " + message); - ctx.response().setStatusCode(status).end(new JsonObject().put("error", message).encode()); - } -}