up, add OLAP columns page
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
263
src/main/java/su/xserver/iikocon/iiko/IikoHandler.java
Normal file
263
src/main/java/su/xserver/iikocon/iiko/IikoHandler.java
Normal file
@@ -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<JsonObject> 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<Void> 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<Void> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<String> authenticate() {
|
||||
Promise<String> 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;
|
||||
|
||||
@@ -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<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) {
|
||||
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<Void> 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<JsonObject> fetchColumnsFromIiko(String reportType) {
|
||||
Promise<JsonObject> 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<Void> storeColumnsToDb(String reportType, JsonObject columns) {
|
||||
return getOrCreateReportType(reportType)
|
||||
.compose(reportTypeId -> {
|
||||
@@ -82,86 +61,86 @@ public class IikoOlapColumnsImporter {
|
||||
}
|
||||
|
||||
private Future<Integer> getOrCreateReportType(String reportType) {
|
||||
Promise<Integer> 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<Void> 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<Integer> 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<Void> 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<Void> processTags(int fieldId, JsonArray tags) {
|
||||
List<Future<Void>> tagFutures = new ArrayList<>();
|
||||
for (Object tagObj : tags) {
|
||||
@@ -173,93 +152,25 @@ public class IikoOlapColumnsImporter {
|
||||
}
|
||||
|
||||
private Future<Integer> getOrCreateTag(String tagName) {
|
||||
Promise<Integer> 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<Void> 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<Void> 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) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Void> 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user