From 59e283945cfb8ef2d77e024de3c5d4ea892ff5b7 Mon Sep 17 00:00:00 2001 From: Danil-Bodry Date: Thu, 7 May 2026 17:36:03 +0300 Subject: [PATCH] up --- frontend/src/router/index.ts | 5 -- frontend/src/views/OlapConstructor.vue | 31 +++++-- frontend/src/views/OlapQueriesPage.vue | 14 +++ .../java/su/xserver/iikocon/MainVerticle.java | 40 ++++++++- .../iikocon/iiko/OlapQueryService.java | 85 ++++++++++++------- 5 files changed, 126 insertions(+), 49 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 37afe17..27ab445 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -66,11 +66,6 @@ const routes = [ component: AdminSettings, meta: { requiresAuth: true, requiresAdmin: true, title: 'Settings' } }, -// { -// path: '/olap-constructor', -// component: OLAPConstructor, -// meta: { requiresAuth: true, title: 'OLAP Constructor' } -// }, { path: '/olap/queries', component: OlapQueriesPage, diff --git a/frontend/src/views/OlapConstructor.vue b/frontend/src/views/OlapConstructor.vue index 079054c..486527a 100644 --- a/frontend/src/views/OlapConstructor.vue +++ b/frontend/src/views/OlapConstructor.vue @@ -135,7 +135,7 @@
- +
@@ -581,7 +581,7 @@ const daysBack = ref(7) const tableName = ref('') const tableNameValid = ref(true) const tableNameTouched = ref(false) -const active = ref(true) +const active = ref(false) const searchQuery = ref('') const activeTab = ref<'table' | 'sql'>('table') const collapsed = ref({ number: false, category: false, filter: false }) @@ -874,6 +874,8 @@ async function loadQuery(id: number) { // 1. Базовые поля запроса queryName.value = data.name + active.value = data.active ?? true; + // 2. Подключение к БД if (data.dbConnectionId) { await loadDbConnections() @@ -974,13 +976,18 @@ async function saveQuery() { showNotification('Выберите хотя бы один ресторан', 'error') return } + if (!tableName.value.trim()) { + showNotification('Укажите название таблицы SQL', 'error'); + return; + } const config = buildConfigObject() const payload = { name: queryName.value, dbConnectionId: selectedDbConnection.value.id, config: config, - restaurantIds: selectedRestaurants.value.map(r => r.id) - } + restaurantIds: selectedRestaurants.value.map(r => r.id), + active: active.value + }; const url = queryId.value ? `/api/olap/queries/${queryId.value}` : '/api/olap/queries' const method = queryId.value ? 'PUT' : 'POST' try { @@ -1011,9 +1018,17 @@ const filteredCategoryFields = computed(() => availableFields.value.filter((f): const filteredFilterFields = computed(() => availableFields.value.filter((f): f is FilterField => f.role === 'filter' && matchesSearch(f))) const validateTableName = () => { - tableNameTouched.value = true - if (!tableName.value) { tableNameValid.value = true; return } - tableNameValid.value = /^[A-Za-zА-Яа-я]/.test(tableName.value) + tableNameTouched.value = true; + const value = tableName.value.trim(); + if (!value) { + tableNameValid.value = false; + return; + } + // Регулярное выражение: + // ^[A-Za-z] - первая буква (только английская) + // [A-Za-z0-9]* - далее любые английские буквы или цифры + const regex = /^[A-Za-z][A-Za-z0-9]*$/; + tableNameValid.value = regex.test(value); } const parseValues = (f: FilterField) => { @@ -1184,7 +1199,7 @@ const confirmReset = () => { refreshFieldsAndReset() dateTo.value = '' daysBack.value = 7 - active.value = true + active.value = false tableName.value = '' tableNameValid.value = true tableNameTouched.value = false diff --git a/frontend/src/views/OlapQueriesPage.vue b/frontend/src/views/OlapQueriesPage.vue index e9de66f..8e542e7 100644 --- a/frontend/src/views/OlapQueriesPage.vue +++ b/frontend/src/views/OlapQueriesPage.vue @@ -12,6 +12,9 @@ ID Название + Активен + Последнее выполнение + Результат Подключение Рестораны Создан @@ -22,6 +25,17 @@ {{ q.id }} {{ q.name }} + + + {{ q.active ? 'Да' : 'Нет' }} + + + {{ q.lastRun ? formatDate(q.lastRun) : '—' }} + + + Успешно + Ошибка + {{ q.dbConnectionName }} {{ q.restaurants }} {{ formatDate(q.created) }} diff --git a/src/main/java/su/xserver/iikocon/MainVerticle.java b/src/main/java/su/xserver/iikocon/MainVerticle.java index 3be79e3..6b36d25 100644 --- a/src/main/java/su/xserver/iikocon/MainVerticle.java +++ b/src/main/java/su/xserver/iikocon/MainVerticle.java @@ -552,14 +552,31 @@ public class MainVerticle extends AbstractVerticle { JsonArray restaurantIdsArray = body.getJsonArray("restaurantIds", new JsonArray()); List restaurantIds = restaurantIdsArray.stream().map(id -> (Integer) id).collect(Collectors.toList()); + // Получаем active: сначала из тела, иначе из конфига, иначе true + Boolean active = body.getBoolean("active"); + if (active == null && config != null) active = config.getBoolean("active", true); + if (active == null) active = true; + if (name == null || dbConnectionId == null || config == null) { rc.response().setStatusCode(400).end("Missing required fields"); return; } - // Сначала генерируем SQL + String tableName = config.getString("tableName"); + + if (tableName.isEmpty()) { + rc.response().setStatusCode(400).end("Missing required fields"); + return; + } + + if (!olapQueryService.isValidTableName(tableName)) { + rc.response().setStatusCode(400).end("Invalid tableName: must start with a letter and contain only letters and digits"); + return; + } + + Boolean finalActive = active; olapQueryService.generateSql(config, dbConnectionId) - .compose(sql -> olapQueryService.createQuery(name, dbConnectionId, config, restaurantIds, sql)) + .compose(sql -> olapQueryService.createQuery(name, dbConnectionId, config, restaurantIds, sql, finalActive)) .onSuccess(id -> rc.response().setStatusCode(201).putHeader("Content-Type", "application/json").end(new JsonObject().put("id", id).encode())) .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); }); @@ -573,13 +590,30 @@ public class MainVerticle extends AbstractVerticle { JsonArray restaurantIdsArray = body.getJsonArray("restaurantIds", new JsonArray()); List restaurantIds = restaurantIdsArray.stream().map(v -> (Integer) v).collect(Collectors.toList()); + Boolean active = body.getBoolean("active"); + if (active == null && config != null) active = config.getBoolean("active", true); + if (active == null) active = true; + if (name == null || dbConnectionId == null || config == null) { rc.response().setStatusCode(400).end("Missing required fields"); return; } + String tableName = config.getString("tableName"); + + if (tableName.isEmpty()) { + rc.response().setStatusCode(400).end("Missing required fields"); + return; + } + + if (!olapQueryService.isValidTableName(tableName)) { + rc.response().setStatusCode(400).end("Invalid tableName: must start with a letter and contain only letters and digits"); + return; + } + + Boolean finalActive = active; olapQueryService.generateSql(config, dbConnectionId) - .compose(sql -> olapQueryService.updateQuery(id, name, dbConnectionId, config, restaurantIds, sql)) + .compose(sql -> olapQueryService.updateQuery(id, name, dbConnectionId, config, restaurantIds, sql, finalActive)) .onSuccess(v -> rc.response().end()) .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); }); diff --git a/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java b/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java index f219d0f..dd7c864 100644 --- a/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java +++ b/src/main/java/su/xserver/iikocon/iiko/OlapQueryService.java @@ -5,6 +5,7 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.Tuple; import io.vertx.sqlclient.templates.SqlTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +34,9 @@ public class OlapQueryService { config_json JSON NOT NULL, full_config_json JSON NOT NULL, sql_text TEXT, + active BOOLEAN NOT NULL DEFAULT true, + last_run TIMESTAMP NULL, + last_run_success BOOLEAN NULL, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (db_connection_id) REFERENCES external_database(id) ON DELETE RESTRICT @@ -56,16 +60,17 @@ public class OlapQueryService { // Создание запроса public Future createQuery(String name, int dbConnectionId, JsonObject config, - List restaurantIds, String generatedSql) { + List restaurantIds, String generatedSql, boolean active) { JsonObject fullConfig = generateFullIikoJson(config); Map params = Map.of( "name", name, "db_connection_id", dbConnectionId, "config_json", config.encode(), "full_config_json", fullConfig.encode(), - "sql_text", generatedSql != null ? generatedSql : "" + "sql_text", generatedSql != null ? generatedSql : "", + "active", active ); - String sql = "INSERT INTO olap_queries (name, db_connection_id, config_json, full_config_json, sql_text) VALUES (#{name}, #{db_connection_id}, #{config_json}, #{full_config_json}, #{sql_text})"; + String sql = "INSERT INTO olap_queries (name, db_connection_id, config_json, full_config_json, sql_text, active) VALUES (#{name}, #{db_connection_id}, #{config_json}, #{full_config_json}, #{sql_text}, #{active})"; return SqlTemplate.forUpdate(pool, sql) .execute(params) .compose(rows -> getLastInsertId()) @@ -73,7 +78,7 @@ public class OlapQueryService { } public Future updateQuery(int id, String name, int dbConnectionId, JsonObject config, - List restaurantIds, String generatedSql) { + List restaurantIds, String generatedSql, boolean active) { JsonObject fullConfig = generateFullIikoJson(config); Map params = Map.of( "id", id, @@ -81,15 +86,14 @@ public class OlapQueryService { "db_connection_id", dbConnectionId, "config_json", config.encode(), "full_config_json", fullConfig.encode(), - "sql_text", generatedSql != null ? generatedSql : "" + "sql_text", generatedSql != null ? generatedSql : "", + "active", active ); - String sql = "UPDATE olap_queries SET name = #{name}, db_connection_id = #{db_connection_id}, config_json = #{config_json}, full_config_json = #{full_config_json}, sql_text = #{sql_text} WHERE id = #{id}"; + String sql = "UPDATE olap_queries SET name = #{name}, db_connection_id = #{db_connection_id}, config_json = #{config_json}, full_config_json = #{full_config_json}, sql_text = #{sql_text}, active = #{active} WHERE id = #{id}"; return SqlTemplate.forUpdate(pool, sql) .execute(params) - .compose(v -> { - return pool.query("DELETE FROM olap_query_restaurants WHERE query_id = " + id).execute() - .compose(del -> linkRestaurants(id, restaurantIds)); - }).mapEmpty(); + .compose(v -> pool.query("DELETE FROM olap_query_restaurants WHERE query_id = " + id).execute() + .compose(del -> linkRestaurants(id, restaurantIds))).mapEmpty(); } public JsonObject generateFullIikoJson(JsonObject clientConfig) { @@ -139,14 +143,13 @@ public class OlapQueryService { // Добавляем системные allFilters.mergeIn(systemFilters); - JsonObject result = new JsonObject() + return new JsonObject() .put("reportType", reportType) .put("buildSummary", buildSummary) .put("groupByRowFields", rowFields) .put("groupByColFields", columnFields) .put("aggregateFields", valueFields) .put("filters", allFilters); - return result; } private JsonObject buildDateFilter(String reportType, String dateToStr, int daysBack) { @@ -195,36 +198,38 @@ public class OlapQueryService { // Получить все запросы (без config_json, для списка) public Future getAllQueries() { String sql = """ - SELECT q.id, q.name, q.db_connection_id, q.created, q.updated, - GROUP_CONCAT(r.name SEPARATOR ', ') AS restaurants, - dc.name AS db_connection_name - FROM olap_queries q - LEFT JOIN olap_query_restaurants qr ON q.id = qr.query_id - LEFT JOIN restaurants r ON qr.restaurant_id = r.id - LEFT JOIN external_database dc ON q.db_connection_id = dc.id - GROUP BY q.id - ORDER BY q.id DESC - """; + SELECT q.id, q.name, q.db_connection_id, q.active, q.last_run, q.last_run_success, q.created, q.updated, + GROUP_CONCAT(r.name SEPARATOR ', ') AS restaurants, + dc.name AS db_connection_name + FROM olap_queries q + LEFT JOIN olap_query_restaurants qr ON q.id = qr.query_id + LEFT JOIN restaurants r ON qr.restaurant_id = r.id + LEFT JOIN external_database dc ON q.db_connection_id = dc.id + GROUP BY q.id + ORDER BY q.id DESC + """; return pool.query(sql).execute() .map(rows -> { JsonArray arr = new JsonArray(); - rows.forEach(row -> { - arr.add(new JsonObject() - .put("id", row.getInteger("id")) - .put("name", row.getString("name")) - .put("dbConnectionId", row.getInteger("db_connection_id")) - .put("dbConnectionName", row.getString("db_connection_name")) - .put("restaurants", row.getString("restaurants") != null ? row.getString("restaurants") : "") - .put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null) - .put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null)); - }); + rows.forEach(row -> arr.add(new JsonObject() + .put("id", row.getInteger("id")) + .put("name", row.getString("name")) + .put("dbConnectionId", row.getInteger("db_connection_id")) + .put("dbConnectionName", row.getString("db_connection_name")) + .put("restaurants", row.getString("restaurants") != null ? row.getString("restaurants") : "") + .put("active", row.getBoolean("active")) + .put("lastRun", row.getLocalDateTime("last_run") != null ? row.getLocalDateTime("last_run").toString() : null) + .put("lastRunSuccess", row.getBoolean("last_run_success")) + .put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null) + .put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null))); return arr; }); } + // Получить один запрос с полной конфигурацией public Future getQueryById(int id) { - String querySql = "SELECT id, name, db_connection_id, config_json, sql_text, created, updated FROM olap_queries WHERE id = ?"; + String querySql = "SELECT id, name, db_connection_id, config_json, sql_text, active, created, updated FROM olap_queries WHERE id = ?"; String restaurantsSql = "SELECT restaurant_id FROM olap_query_restaurants WHERE query_id = ?"; return pool.preparedQuery(querySql).execute(io.vertx.sqlclient.Tuple.of(id)) @@ -237,6 +242,7 @@ public class OlapQueryService { .put("dbConnectionId", row.getInteger("db_connection_id")) .put("config", new JsonObject(row.getString("config_json"))) .put("sql", row.getString("sql_text")) + .put("active", row.getBoolean("active")) .put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null) .put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null); @@ -250,6 +256,12 @@ public class OlapQueryService { }); } + // Метод для обновления статуса выполнения + public Future updateRunStatus(int queryId, boolean success) { + String sql = "UPDATE olap_queries SET last_run = NOW(), last_run_success = ? WHERE id = ?"; + return pool.preparedQuery(sql).execute(Tuple.of(success, queryId)).mapEmpty(); + } + // Генерация SQL на основе конфигурации и ID подключения public Future generateSql(JsonObject config, int dbConnectionId) { return externalDataBaseService.findById(dbConnectionId) @@ -264,4 +276,11 @@ public class OlapQueryService { } }); } + + public boolean isValidTableName(String tableName) { + if (tableName == null) return false; + String trimmed = tableName.trim(); + // Первый символ — английская буква, далее буквы или цифры + return trimmed.matches("^[A-Za-z][A-Za-z0-9]*$"); + } }