diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp index c4a4880..829b02e 100644 --- a/android/adb/AdbUtils.cpp +++ b/android/adb/AdbUtils.cpp @@ -15,14 +15,10 @@ QString AdbUtils::adbPath() { #endif } -bool AdbUtils::startProcess(const QString &cmd, QString *stdOut = nullptr, QString *stdErr = nullptr) { +bool AdbUtils::startProcess(const QString &program, const QStringList &args, + QString *stdOut, QString *stdErr) { QProcess process; - -#ifdef Q_OS_WIN - process.start("cmd.exe", {"/C", cmd}); -#else - process.start("sh", {"-c", cmd}); -#endif + process.start(program, args); if (!process.waitForFinished() || process.exitStatus() != QProcess::NormalExit || @@ -130,41 +126,34 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool scre QString output; // 1. Dump UI hierarchy - QString dumpCmd = QString("%1 -s %2 shell uiautomator dump /sdcard/uidump.xml").arg(adb, deviceId); - if (!startProcess(dumpCmd)) { + if (!startProcess(adb, {"-s", deviceId, "shell", "uiautomator", "dump", "/sdcard/uidump.xml"})) { qWarning() << "Failed to dump UI hierarchy."; return {}; } // 2. Optionally pull and save the XML file if (screenshot) { - QString pullCmd = QString("%1 -s %2 pull /sdcard/uidump.xml step_%3.xml").arg(adb, deviceId).arg(idx); - if (!startProcess(pullCmd)) { + if (!startProcess(adb, {"-s", deviceId, "pull", "/sdcard/uidump.xml", + QString("step_%1.xml").arg(idx)})) { qWarning() << "Failed to pull UIDump XML."; return {}; } } // 3. Read the XML content - QString catCmd = QString("%1 -s %2 shell cat /sdcard/uidump.xml").arg(adb, deviceId); QString xmlOut; - if (!startProcess(catCmd, &xmlOut)) { + if (!startProcess(adb, {"-s", deviceId, "shell", "cat", "/sdcard/uidump.xml"}, &xmlOut)) { qWarning() << "Failed to read UIDump XML."; return {}; } // 4. Remove temp XML file - QString rmCmd = QString("%1 -s %2 shell rm /sdcard/uidump.xml").arg(adb, deviceId); - startProcess(rmCmd); // неважно, успешна ли команда + startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/uidump.xml"}); // неважно, успешна ли команда // 5. Screenshot (if needed) if (screenshot) { - QString shotCmds[] = { - QString("%1 -s %2 shell screencap -p /sdcard/screen.png").arg(adb, deviceId), - QString("%1 -s %2 pull /sdcard/screen.png screen_step_%3.png").arg(adb, deviceId).arg(idx), - QString("%1 -s %2 shell rm /sdcard/screen.png").arg(adb, deviceId) - }; - for (const QString &cmd: shotCmds) { - startProcess(cmd); // ошибки игнорируем для скриншота - } + startProcess(adb, {"-s", deviceId, "shell", "screencap", "-p", "/sdcard/screen.png"}); + startProcess(adb, {"-s", deviceId, "pull", "/sdcard/screen.png", + QString("screen_step_%1.png").arg(idx)}); + startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/screen.png"}); } // 6. Обрезка к началу XML @@ -177,16 +166,17 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool scre bool AdbUtils::takeScreenshot(const QString &deviceId, const QString &name) { const QString adb = adbPath(); - QStringList commands = { - QString("%1 -s %2 shell screencap -p /sdcard/screen.png").arg(adb, deviceId), - QString("%1 -s %2 pull /sdcard/screen.png %3.png").arg(adb, deviceId, name), - QString("%1 -s %2 shell rm /sdcard/screen.png").arg(adb, deviceId) + struct Step { QStringList args; }; + Step steps[] = { + {{"-s", deviceId, "shell", "screencap", "-p", "/sdcard/screen.png"}}, + {{"-s", deviceId, "pull", "/sdcard/screen.png", name + ".png"}}, + {{"-s", deviceId, "shell", "rm", "/sdcard/screen.png"}} }; - for (const QString &cmd: commands) { + for (const auto &step : steps) { QString stdErr; - if (!startProcess(cmd, nullptr, &stdErr)) { - qWarning() << "ADB command failed:" << cmd; + if (!startProcess(adb, step.args, nullptr, &stdErr)) { + qWarning() << "ADB command failed:" << step.args.join(' '); qWarning() << "Error:" << stdErr; return false; } @@ -195,13 +185,8 @@ bool AdbUtils::takeScreenshot(const QString &deviceId, const QString &name) { } QByteArray AdbUtils::captureScreenshotBytes(const QString &deviceId) { - const QString cmd = QString("%1 -s %2 exec-out screencap -p").arg(adbPath(), deviceId); QProcess process; -#ifdef Q_OS_WIN - process.start("cmd.exe", {"/C", cmd}); -#else - process.start("sh", {"-c", cmd}); -#endif + process.start(adbPath(), {"-s", deviceId, "exec-out", "screencap", "-p"}); if (!process.waitForFinished(10000) || process.exitCode() != 0) { qWarning() << "[AdbUtils] captureScreenshotBytes failed for" << deviceId; return {}; @@ -211,17 +196,18 @@ QByteArray AdbUtils::captureScreenshotBytes(const QString &deviceId) { bool AdbUtils::takeXmlDump(const QString &deviceId, const QString &name) { const QString adb = adbPath(); - QStringList commands = { - QString("%1 -s %2 shell uiautomator dump /sdcard/uidump.xml").arg(adb, deviceId), - QString("%1 -s %2 pull /sdcard/uidump.xml %3.xml").arg(adb, deviceId, name), - QString("%1 -s %2 shell cat /sdcard/uidump.xml").arg(adb, deviceId), - QString("%1 -s %2 shell rm /sdcard/uidump.xml").arg(adb, deviceId) + struct Step { QStringList args; }; + Step steps[] = { + {{"-s", deviceId, "shell", "uiautomator", "dump", "/sdcard/uidump.xml"}}, + {{"-s", deviceId, "pull", "/sdcard/uidump.xml", name + ".xml"}}, + {{"-s", deviceId, "shell", "cat", "/sdcard/uidump.xml"}}, + {{"-s", deviceId, "shell", "rm", "/sdcard/uidump.xml"}} }; - for (const QString &cmd: commands) { + for (const auto &step : steps) { QString stdErr; - if (!startProcess(cmd, nullptr, &stdErr)) { - qWarning() << "ADB command failed:" << cmd; + if (!startProcess(adb, step.args, nullptr, &stdErr)) { + qWarning() << "ADB command failed:" << step.args.join(' '); qWarning() << "Error:" << stdErr; return false; } @@ -242,15 +228,16 @@ bool AdbUtils::makeTap(const QString &deviceId, const int x, const int y) { bool AdbUtils::enableAdbIMEKeyboard(const QString &deviceId) { const QString adb = adbPath(); - QStringList commands = { - QString("%1 -s %2 shell ime enable com.android.adbkeyboard/.AdbIME").arg(adb, deviceId), - QString("%1 -s %2 shell ime set com.android.adbkeyboard/.AdbIME").arg(adb, deviceId) + struct Step { QStringList args; }; + Step steps[] = { + {{"-s", deviceId, "shell", "ime", "enable", "com.android.adbkeyboard/.AdbIME"}}, + {{"-s", deviceId, "shell", "ime", "set", "com.android.adbkeyboard/.AdbIME"}} }; - for (const QString &cmd: commands) { + for (const auto &step : steps) { QString stdErr; - if (!startProcess(cmd, nullptr, &stdErr)) { - qWarning() << "ADB command failed:" << cmd; + if (!startProcess(adb, step.args, nullptr, &stdErr)) { + qWarning() << "ADB command failed:" << step.args.join(' '); qWarning() << "Error:" << stdErr; return false; } @@ -275,10 +262,8 @@ bool AdbUtils::inputAdbIMEText(const QString &deviceId, const QString &text) { bool AdbUtils::disableAdbIMEKeyboard(const QString &deviceId) { QProcess process; - QString command = QString( - "%1 -s %2 shell ime disable com.android.adbkeyboard/.AdbIME" - ).arg(adbPath(), deviceId); - process.start(command); + process.start(adbPath(), {"-s", deviceId, "shell", "ime", "disable", + "com.android.adbkeyboard/.AdbIME"}); process.waitForFinished(); return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; } @@ -290,7 +275,9 @@ bool AdbUtils::makeSwipe(const QString &deviceId, const int x1, const int y1, co { "-s", deviceId, "shell", "input", - "swipe", QString("%1 %2 %3 %4").arg(x1).arg(y1).arg(x2).arg(y2) + "swipe", + QString::number(x1), QString::number(y1), + QString::number(x2), QString::number(y2) } ); process.waitForFinished(); diff --git a/android/adb/AdbUtils.h b/android/adb/AdbUtils.h index 0dc639d..5cbbb23 100644 --- a/android/adb/AdbUtils.h +++ b/android/adb/AdbUtils.h @@ -47,5 +47,6 @@ public: static QSet getListApps(const QString &deviceId); private: - static bool startProcess(const QString &cmd, QString *stdOut, QString *stdErr); + static bool startProcess(const QString &program, const QStringList &args, + QString *stdOut = nullptr, QString *stdErr = nullptr); }; diff --git a/android/ozon/CommonScript.cpp b/android/ozon/CommonScript.cpp index 4a23c2f..c20daa3 100644 --- a/android/ozon/CommonScript.cpp +++ b/android/ozon/CommonScript.cpp @@ -110,6 +110,14 @@ bool CommonScript::runAppAndGoToHomeScreen( continue; } + // Проверяем на рекламный bottom sheet (ипотека и т.п.) + if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + qDebug() << "[runAppAndGoToHomeScreen] Ad bottom sheet detected, closing..."; + AdbUtils::makeTap(deviceId, adNode.x(), adNode.y()); + QThread::msleep(1000); + continue; + } + // Проверяем, что это экран ввода пин-кода if (!findAndInputPincode) { if (const QList pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp index e653038..68c15e9 100644 --- a/android/ozon/GetCardInfoScript.cpp +++ b/android/ozon/GetCardInfoScript.cpp @@ -10,6 +10,7 @@ #include "dao/AccountDAO.h" #include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" +#include "dao/SettingsDAO.h" #include "net/NetworkService.h" #include "AppLogger.h" @@ -155,7 +156,7 @@ void GetCardInfoScript::doStart() { // 1. Создаём девайс если нет api_id if (device.apiId.isEmpty()) { QJsonObject deviceBody; - deviceBody["desktop_id"] = QSettings("config.ini", QSettings::IniFormat).value("common/desktop_id").toString(); + deviceBody["desktop_id"] = SettingsDAO::get("desktop_id"); deviceBody["status"] = "ONLINE"; deviceBody["name"] = device.name; deviceBody["screen_width"] = device.screenWidth; @@ -261,12 +262,18 @@ void GetCardInfoScript::doStart() { break; } - // Закрываем баннер "Понятно" если он перекрывает экран + // Закрываем баннер "Понятно" и повторно нажимаем "Показать" Node bannerBtn = xmlScreenParser.findTextNode("Понятно"); if (!bannerBtn.isEmpty()) { qDebug() << "[GetCardInfo] Banner 'Понятно' detected, closing..."; AdbUtils::makeTap(m_deviceId, bannerBtn.x(), bannerBtn.y()); - QThread::msleep(1000); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node retryShowBtn = xmlScreenParser.findButtonNode("Показать", false); + if (!retryShowBtn.isEmpty()) { + qDebug() << "[GetCardInfo] Re-tapping 'Показать' after banner close"; + AdbUtils::makeTap(m_deviceId, retryShowBtn.x(), retryShowBtn.y()); + } continue; } diff --git a/android/ozon/ScreenXmlParser.cpp b/android/ozon/ScreenXmlParser.cpp index 32f0ba9..4480ab2 100644 --- a/android/ozon/ScreenXmlParser.cpp +++ b/android/ozon/ScreenXmlParser.cpp @@ -412,22 +412,46 @@ QList ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { return result; } - // Способ 2 (fallback): анонимные FrameLayout — ищем сетку 4x3 - QList clickable; + // Способ 2 (fallback): группируем clickable-ноды одинакового размера, + // ищем сетку 4x3 по паттерну (не привязываясь к координатам экрана) + QMap, QList> sizeGroups; for (const Node &node : m_nodes) { - if (node.clickable && node.width() > 0 && node.height() > 0) - clickable.append(node); + if (!node.clickable || node.width() <= 0 || node.height() <= 0) continue; + if (node.width() == node.height()) { // кнопки пин-пада квадратные + sizeGroups[{node.width(), node.height()}].append(node); + } } - if (clickable.isEmpty()) return {}; - - int minH = INT_MAX; - for (const Node &n : clickable) minH = std::min(minH, n.height()); + // Ищем группу из >= 12 одинаковых кнопок (пин-пад: 3x4 = 12) QList candidates; - for (const Node &n : clickable) { - if (n.height() <= minH * 3) candidates.append(n); + for (auto it = sizeGroups.begin(); it != sizeGroups.end(); ++it) { + if (it.value().size() >= 12 && + (candidates.isEmpty() || it.value().size() < candidates.size())) { + candidates = it.value(); + } } + if (candidates.size() < 12) { + // Попробуем без ограничения на квадрат, но с >= 12 + sizeGroups.clear(); + for (const Node &node : m_nodes) { + if (!node.clickable || node.width() <= 0 || node.height() <= 0) continue; + sizeGroups[{node.width(), node.height()}].append(node); + } + for (auto it = sizeGroups.begin(); it != sizeGroups.end(); ++it) { + if (it.value().size() >= 12 && + (candidates.size() < 12 || it.value().size() < candidates.size())) { + candidates = it.value(); + } + } + } + + if (candidates.size() < 12) { + qWarning() << "[findPinCodeNodes] grid not found: no group of 12+ same-sized clickable nodes"; + return {}; + } + + // Сортируем по Y, затем группируем в строки std::sort(candidates.begin(), candidates.end(), [](const Node &a, const Node &b) { return a.y() < b.y(); }); @@ -457,7 +481,8 @@ QList ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { } if (gridRows.size() < 4) { - qWarning() << "[findPinCodeNodes] grid not found: gridRows=" << gridRows.size(); + qWarning() << "[findPinCodeNodes] grid not found: gridRows=" << gridRows.size() + << "candidates=" << candidates.size(); return {}; } diff --git a/assets/ozon/banner_ipoteka.png b/assets/ozon/banner_ipoteka.png new file mode 100644 index 0000000..f437b28 Binary files /dev/null and b/assets/ozon/banner_ipoteka.png differ diff --git a/assets/ozon/banner_ipoteka.xml b/assets/ozon/banner_ipoteka.xml new file mode 100644 index 0000000..ae74225 --- /dev/null +++ b/assets/ozon/banner_ipoteka.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pin_code.xml b/assets/ozon/pin_code.xml index 45b2465..62e5eda 100644 --- a/assets/ozon/pin_code.xml +++ b/assets/ozon/pin_code.xml @@ -66,14 +66,189 @@ focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2337]" drawing-order="1" hint=""> - + selected="false" bounds="[0,0][1080,2337]" drawing-order="0" + hint=""> + + + + + + + + + + + + + + + + + + + + + + + + + #include #include +#include DatabaseManager::DatabaseManager() = default; @@ -75,6 +76,13 @@ QString DatabaseManager::connectionName() { static bool columnExists(QSqlQuery &q, const QString &table, const QString &column) { + // Валидация: допускаем только [a-zA-Z0-9_] чтобы исключить SQL injection + static const QRegularExpression valid("^[a-zA-Z_][a-zA-Z0-9_]*$"); + if (!valid.match(table).hasMatch() || !valid.match(column).hasMatch()) { + qCritical() << "[DB] columnExists: invalid identifier" << table << column; + return false; + } + q.exec(QString("SELECT COUNT(*) FROM pragma_table_info('%1') WHERE name='%2'") .arg(table, column)); return q.next() && q.value(0).toInt() > 0; @@ -88,7 +96,7 @@ void DatabaseManager::initSchema() { q.exec("PRAGMA user_version"); const int version = q.next() ? q.value(0).toInt() : 0; - // ── Версия 1: начальная схема + api_id для devices ──────────────────────── + // ── Версия 1: полная схема ───────────────────────────────────────────────── if (version < 1) { db.transaction(); @@ -117,6 +125,11 @@ void DatabaseManager::initSchema() { status TEXT DEFAULT 'off', install TEXT DEFAULT 'no', comment TEXT, + phone TEXT DEFAULT '', + full_name TEXT DEFAULT '', + bank_profile_id TEXT DEFAULT '', + currency TEXT DEFAULT '', + amount REAL DEFAULT 0, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, UNIQUE (device_id, code) ))"); @@ -131,6 +144,8 @@ void DatabaseManager::initSchema() { currency TEXT DEFAULT '', update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), status TEXT DEFAULT 'active', + card_number TEXT DEFAULT '', + material_id TEXT DEFAULT '', FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, UNIQUE (device_id, app_code, last_numbers) ))"); @@ -155,185 +170,11 @@ void DatabaseManager::initSchema() { update_time DATETIME, complete_time DATETIME, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + receipt_image BLOB, FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE ))"); q.exec(R"(CREATE TABLE IF NOT EXISTS events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - account_id INTEGER, - external_id INTEGER, - device_id TEXT, - amount REAL, - phone TEXT, - bank_name TEXT, - comment TEXT, - status TEXT, - type TEXT, - update_time DATETIME, - complete_time DATETIME, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE, - FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, - UNIQUE (external_id) - ))"); - - // Добавляем api_id в devices если ещё нет (для существующих БД) - if (!columnExists(q, "devices", "api_id")) { - q.exec("ALTER TABLE devices ADD COLUMN api_id TEXT"); - } - - db.commit(); - q.exec("PRAGMA user_version = 1"); - qDebug() << "[DB] Migration 1 applied"; - } - - // ── Версия 2: currency в accounts + переименование app_name → app_code ─── - if (version < 2) { - db.transaction(); - - // Добавляем currency если ещё нет (существующие БД без этого поля) - if (!columnExists(q, "accounts", "currency")) { - q.exec("ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT ''"); - } - - // Переименовываем app_name → app_code (SQLite 3.25+) - if (columnExists(q, "accounts", "app_name")) { - q.exec("ALTER TABLE accounts RENAME COLUMN app_name TO app_code"); - if (q.lastError().isValid()) { - qWarning() << "[DB] Migration 2: rename app_name failed:" << q.lastError().text(); - } - } - - db.commit(); - q.exec("PRAGMA user_version = 2"); - qDebug() << "[DB] Migration 2 applied"; - } - - // ── Версия 3: phone и full_name в applications ──────────────────────────── - if (version < 3) { - db.transaction(); - - if (!columnExists(q, "applications", "phone")) { - q.exec("ALTER TABLE applications ADD COLUMN phone TEXT DEFAULT ''"); - } - if (!columnExists(q, "applications", "full_name")) { - q.exec("ALTER TABLE applications ADD COLUMN full_name TEXT DEFAULT ''"); - } - - db.commit(); - q.exec("PRAGMA user_version = 3"); - qDebug() << "[DB] Migration 3 applied"; - } - - // ── Версия 4: card_number в accounts ────────────────────────────────────── - if (version < 4) { - db.transaction(); - - if (!columnExists(q, "accounts", "card_number")) { - q.exec("ALTER TABLE accounts ADD COLUMN card_number TEXT DEFAULT ''"); - } - - db.commit(); - q.exec("PRAGMA user_version = 4"); - qDebug() << "[DB] Migration 4 applied"; - } - - // ── Версия 5: bank_profile_id в applications ────────────────────────────── - if (version < 5) { - db.transaction(); - - if (!columnExists(q, "applications", "bank_profile_id")) { - q.exec("ALTER TABLE applications ADD COLUMN bank_profile_id TEXT DEFAULT ''"); - } - - db.commit(); - q.exec("PRAGMA user_version = 5"); - qDebug() << "[DB] Migration 5 applied"; - } - - // ── Версия 6: currency и amount в applications ───────────────────────────── - if (version < 6) { - db.transaction(); - - if (!columnExists(q, "applications", "currency")) { - q.exec("ALTER TABLE applications ADD COLUMN currency TEXT DEFAULT ''"); - } - if (!columnExists(q, "applications", "amount")) { - q.exec("ALTER TABLE applications ADD COLUMN amount REAL DEFAULT 0"); - } - - db.commit(); - q.exec("PRAGMA user_version = 6"); - qDebug() << "[DB] Migration 6 applied"; - } - - // ── Версия 7: material_id в accounts ────────────────────────────────────── - if (version < 7) { - db.transaction(); - - if (!columnExists(q, "accounts", "material_id")) { - q.exec("ALTER TABLE accounts ADD COLUMN material_id TEXT DEFAULT ''"); - } - - db.commit(); - q.exec("PRAGMA user_version = 7"); - qDebug() << "[DB] Migration 7 applied"; - } - - // ── Версия 8: receipt_image в transactions ────────────────────────────────── - if (version < 8) { - db.transaction(); - - if (!columnExists(q, "transactions", "receipt_image")) { - q.exec("ALTER TABLE transactions ADD COLUMN receipt_image BLOB"); - } - - db.commit(); - q.exec("PRAGMA user_version = 8"); - qDebug() << "[DB] Migration 8 applied"; - } - - // ── Версия 9: таблица banks ─────────────────────────────────────────────── - if (version < 9) { - db.transaction(); - - q.exec(R"(CREATE TABLE IF NOT EXISTS banks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - alias TEXT UNIQUE, - name TEXT, - nspk_name TEXT - ))"); - - db.commit(); - q.exec("PRAGMA user_version = 9"); - qDebug() << "[DB] Migration 9 applied"; - } - - // ── Версия 10: таблица app_logs ─────────────────────────────────────────── - if (version < 10) { - db.transaction(); - - q.exec(R"(CREATE TABLE IF NOT EXISTS app_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - source TEXT NOT NULL DEFAULT '', - device_id TEXT NOT NULL DEFAULT '', - message TEXT NOT NULL, - screenshot BLOB, - timestamp TEXT NOT NULL - ))"); - - q.exec("CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC)"); - - db.commit(); - q.exec("PRAGMA user_version = 10"); - qDebug() << "[DB] Migration 10 applied"; - } - - // ── Версия 11: убираем UNIQUE(external_id) из events ───────────────────── - if (version < 11) { - db.transaction(); - - q.exec(R"(CREATE TABLE IF NOT EXISTS events_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER, external_id TEXT, @@ -347,29 +188,38 @@ void DatabaseManager::initSchema() { update_time DATETIME, complete_time DATETIME, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + json TEXT DEFAULT '', + sent_to_server INTEGER DEFAULT 0, FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE ))"); - q.exec(R"(INSERT INTO events_new - SELECT * FROM events)"); + q.exec(R"(CREATE TABLE IF NOT EXISTS banks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alias TEXT UNIQUE, + name TEXT, + nspk_name TEXT + ))"); - q.exec("DROP TABLE events"); - q.exec("ALTER TABLE events_new RENAME TO events"); + q.exec(R"(CREATE TABLE IF NOT EXISTS app_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source TEXT NOT NULL DEFAULT '', + device_id TEXT NOT NULL DEFAULT '', + message TEXT NOT NULL, + screenshot BLOB, + timestamp TEXT NOT NULL + ))"); + + q.exec("CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC)"); + + q.exec(R"(CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT + ))"); db.commit(); - q.exec("PRAGMA user_version = 11"); - qDebug() << "[DB] Migration 11 applied"; - } - - // ── Версия 12: json и sent_to_server в events ──────────────────────────── - if (version < 12) { - db.transaction(); - q.exec("ALTER TABLE events ADD COLUMN json TEXT DEFAULT ''"); - q.exec("ALTER TABLE events ADD COLUMN sent_to_server INTEGER DEFAULT 0"); - db.commit(); - q.exec("PRAGMA user_version = 12"); - qDebug() << "[DB] Migration 12 applied"; + q.exec("PRAGMA user_version = 1"); + qDebug() << "[DB] Migration 1 applied"; } } diff --git a/database/app_data.db b/database/app_data.db index c2ba19f..8ad645a 100644 Binary files a/database/app_data.db and b/database/app_data.db differ diff --git a/database/dao/AccountDAO.cpp b/database/dao/AccountDAO.cpp index e4b3c8b..5a82a26 100644 --- a/database/dao/AccountDAO.cpp +++ b/database/dao/AccountDAO.cpp @@ -58,7 +58,7 @@ bool AccountDAO::updateAccountById( query.bindValue(":status", status); query.bindValue(":description", description); - query.bindValue(":last_numbers", description.right(4)); + query.bindValue(":last_numbers", description.length() >= 4 ? description.right(4) : description); query.bindValue(":currency", currency); query.bindValue(":id", id); diff --git a/database/dao/EventDAO.cpp b/database/dao/EventDAO.cpp index 73c0ff2..d6989bc 100644 --- a/database/dao/EventDAO.cpp +++ b/database/dao/EventDAO.cpp @@ -203,7 +203,10 @@ QList EventDAO::getEvents(int page, int pageSize) { int EventDAO::getTotalCount() { QSqlQuery query(DatabaseManager::instance().database()); - query.exec("SELECT COUNT(*) FROM events"); + if (!query.exec("SELECT COUNT(*) FROM events")) { + qWarning() << "Ошибка при получении getTotalCount:" << query.lastError().text(); + return 0; + } if (query.next()) return query.value(0).toInt(); return 0; } diff --git a/database/dao/SettingsDAO.cpp b/database/dao/SettingsDAO.cpp new file mode 100644 index 0000000..6f0b0ce --- /dev/null +++ b/database/dao/SettingsDAO.cpp @@ -0,0 +1,55 @@ +#include "SettingsDAO.h" + +#include +#include +#include + +#include "DatabaseManager.h" + +QString SettingsDAO::get(const QString &key, const QString &defaultValue) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("SELECT value FROM settings WHERE key = :key LIMIT 1"); + query.bindValue(":key", key); + if (!query.exec()) { + qWarning() << "SettingsDAO::get error:" << query.lastError().text(); + return defaultValue; + } + if (query.next()) { + return query.value(0).toString(); + } + return defaultValue; +} + +void SettingsDAO::set(const QString &key, const QString &value) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + INSERT INTO settings (key, value) VALUES (:key, :value) + ON CONFLICT(key) DO UPDATE SET value = excluded.value + )"); + query.bindValue(":key", key); + query.bindValue(":value", value); + if (!query.exec()) { + qWarning() << "SettingsDAO::set error:" << query.lastError().text(); + } +} + +void SettingsDAO::remove(const QString &key) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("DELETE FROM settings WHERE key = :key"); + query.bindValue(":key", key); + if (!query.exec()) { + qWarning() << "SettingsDAO::remove error:" << query.lastError().text(); + } +} + +qint64 SettingsDAO::getLong(const QString &key, qint64 defaultValue) { + const QString val = get(key); + if (val.isEmpty()) return defaultValue; + bool ok; + const qint64 result = val.toLongLong(&ok); + return ok ? result : defaultValue; +} + +void SettingsDAO::setLong(const QString &key, qint64 value) { + set(key, QString::number(value)); +} diff --git a/database/dao/SettingsDAO.h b/database/dao/SettingsDAO.h new file mode 100644 index 0000000..4afb733 --- /dev/null +++ b/database/dao/SettingsDAO.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +class SettingsDAO { +public: + static QString get(const QString &key, const QString &defaultValue = {}); + static void set(const QString &key, const QString &value); + static void remove(const QString &key); + static qint64 getLong(const QString &key, qint64 defaultValue = 0); + static void setLong(const QString &key, qint64 value); +}; diff --git a/main.cpp b/main.cpp index 51c6f00..894a910 100644 --- a/main.cpp +++ b/main.cpp @@ -17,6 +17,7 @@ #include #include "AppLogger.h" +#include "dao/SettingsDAO.h" #include "EventHandler.h" #include "net/NetworkService.h" #include "ozon/PayByPhoneScript.h" @@ -196,11 +197,11 @@ int main(int argc, char *argv[]) { // - неверный api_key [code 0] — показываем окно ввода // - всё ок [code 1] — запускаем приложение // 2. Если нет api_key — показываем окно для ввода - const QSettings settings("config.ini", QSettings::IniFormat); - const QString apiKey = settings.value("common/api_key").toString(); + const QString apiKey = SettingsDAO::get("api_key"); auto showRegistration = [&app]() { auto *registrationWindow = new RegistrationWindow(); + registrationWindow->setAttribute(Qt::WA_DeleteOnClose); registrationWindow->show(); QObject::connect( registrationWindow, &RegistrationWindow::onRegisteredSuccess, registrationWindow, @@ -219,6 +220,7 @@ int main(int argc, char *argv[]) { showRegistration(); } else { auto *loader = new LoaderWindow(); + loader->setAttribute(Qt::WA_DeleteOnClose); loader->show(); QObject::connect(loader, &LoaderWindow::loadingFinished, loader, [loader, &app, showRegistration](const int result) { @@ -227,10 +229,12 @@ int main(int argc, char *argv[]) { "Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.", true ); + errorWindow->setAttribute(Qt::WA_DeleteOnClose); errorWindow->show(); QObject::connect(errorWindow, &ErrorWindow::retryRequested, errorWindow, [errorWindow, &app, showRegistration]() { errorWindow->close(); auto *newLoader = new LoaderWindow(); + newLoader->setAttribute(Qt::WA_DeleteOnClose); newLoader->show(); QObject::connect(newLoader, &LoaderWindow::loadingFinished, newLoader, [newLoader, &app, showRegistration](const int r) { if (r == 1) { @@ -241,14 +245,14 @@ int main(int argc, char *argv[]) { startEventHandler(app); // startPayByPhoneTest(); } else if (r == 0) { - QSettings s("config.ini", QSettings::IniFormat); - s.remove("common/api_key"); + SettingsDAO::remove("api_key"); showRegistration(); } else { auto *err2 = new ErrorWindow( "Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.", false ); + err2->setAttribute(Qt::WA_DeleteOnClose); err2->show(); } newLoader->close(); @@ -262,8 +266,7 @@ int main(int argc, char *argv[]) { startEventHandler(app); // startPayByPhoneTest(); } else if (result == 0) { - QSettings s("config.ini", QSettings::IniFormat); - s.remove("common/api_key"); + SettingsDAO::remove("api_key"); showRegistration(); } loader->close(); diff --git a/services/DeviceScreener.cpp b/services/DeviceScreener.cpp index 9f99746..5248ff8 100644 --- a/services/DeviceScreener.cpp +++ b/services/DeviceScreener.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,7 @@ #include "adb/AdbUtils.h" #include "dao/ApplicationDAO.h" +#include "dao/SettingsDAO.h" DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) { const QSettings settings("config.ini", QSettings::IniFormat); @@ -77,14 +79,21 @@ void DeviceScreener::postDevicesData(const QList &devices) { // локальный цикл — ждём, пока придёт finished() QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QObject::connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); - loop.exec(); // <- здесь и запускается event-loop потока + QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); // обрабатываем результат - if (reply->error() == QNetworkReply::NoError) { + if (!timeout.isActive()) { + qWarning() << "Таймаут postDevicesData:" << m_urlAppUpdate; + reply->abort(); + } else if (reply->error() == QNetworkReply::NoError) { QByteArray raw = reply->readAll(); QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(raw, &err); @@ -103,20 +112,26 @@ void DeviceScreener::postDevicesData(const QList &devices) { QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body) { if (!m_manager) m_manager = new QNetworkAccessManager(this); - const QSettings settings("config.ini", QSettings::IniFormat); QNetworkRequest request(QUrl(m_apiBase + path)); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", - ("Bearer " + settings.value("common/access_token").toString()).toUtf8()); + ("Bearer " + SettingsDAO::get("access_token")).toUtf8()); QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson()); QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); loop.exec(); QJsonValue result; - if (reply->error() == QNetworkReply::NoError) { + if (!timeout.isActive()) { + qCritical() << "DeviceScreener apiPost timeout:" << path; + reply->abort(); + } else if (reply->error() == QNetworkReply::NoError) { const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); if (doc.isObject()) { const QJsonObject root = doc.object(); @@ -133,27 +148,32 @@ QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body) void DeviceScreener::apiPatch(const QString &path, const QJsonObject &body) { if (!m_manager) m_manager = new QNetworkAccessManager(this); - const QSettings settings("config.ini", QSettings::IniFormat); QNetworkRequest request(QUrl(m_apiBase + path)); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", - ("Bearer " + settings.value("common/access_token").toString()).toUtf8()); + ("Bearer " + SettingsDAO::get("access_token")).toUtf8()); QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson()); QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); loop.exec(); - if (reply->error() != QNetworkReply::NoError) { + if (!timeout.isActive()) { + qCritical() << "DeviceScreener apiPatch timeout:" << path; + reply->abort(); + } else if (reply->error() != QNetworkReply::NoError) { qCritical() << "DeviceScreener apiPatch network error:" << reply->errorString(); } reply->deleteLater(); } QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) { - const QSettings settings("config.ini", QSettings::IniFormat); - const QString desktopId = settings.value("common/desktop_id").toString(); + const QString desktopId = SettingsDAO::get("desktop_id"); if (desktopId.isEmpty()) { qWarning() << "createDeviceOnApi: desktop_id не задан"; return {}; @@ -177,11 +197,10 @@ QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) { void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray &image) { if (!m_manager) m_manager = new QNetworkAccessManager(this); - const QSettings settings("config.ini", QSettings::IniFormat); const QString path = "/api/v1/device/add_screenshot/" + apiId; QNetworkRequest request(QUrl(m_apiBase + path)); request.setRawHeader("Authorization", - ("Bearer " + settings.value("common/access_token").toString()).toUtf8()); + ("Bearer " + SettingsDAO::get("access_token")).toUtf8()); auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); QHttpPart filePart; @@ -194,11 +213,18 @@ void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray QNetworkReply *reply = m_manager->post(request, multiPart); multiPart->setParent(reply); QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); loop.exec(); - if (reply->error() != QNetworkReply::NoError) { + if (!timeout.isActive()) { + qWarning() << "postScreenshotToApi timeout for device" << apiId; + reply->abort(); + } else if (reply->error() != QNetworkReply::NoError) { qWarning() << "postScreenshotToApi network error:" << reply->errorString(); } else { const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index ae0c684..97e38cd 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -15,6 +15,7 @@ #include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" #include "dao/EventDAO.h" +#include "dao/SettingsDAO.h" #include "dao/TransactionDAO.h" #include "db/EventInfo.h" @@ -39,7 +40,7 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) { m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString(); m_manager = new QNetworkAccessManager(this); m_token = settings.value("common/token").toString(); - m_accessToken = settings.value("common/access_token").toString(); + m_accessToken = SettingsDAO::get("access_token"); } NetworkService::~NetworkService() = default; @@ -74,14 +75,21 @@ QJsonValue NetworkService::post(QHttpMultiPart *multiPart, const QString &path) multiPart->setParent(reply); QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); - loop.exec(); // <- здесь и запускается event-loop потока + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); QJsonValue result; - if (reply->error() == QNetworkReply::NoError) { + if (!timeout.isActive()) { + qCritical() << "Таймаут запроса:" << path; + reply->abort(); + } else if (reply->error() == QNetworkReply::NoError) { const QByteArray raw = reply->readAll(); QJsonParseError err; const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); @@ -113,14 +121,21 @@ QJsonArray NetworkService::get(const QString &path, const QMap QScopedPointer guard(reply); QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); - loop.exec(); // <- здесь и запускается event-loop потока + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); QJsonArray result; - if (reply->error() == QNetworkReply::NoError) { + if (!timeout.isActive()) { + qCritical() << "Таймаут запроса (GET):" << path; + reply->abort(); + } else if (reply->error() == QNetworkReply::NoError) { const QByteArray raw = reply->readAll(); QJsonParseError err; const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); @@ -205,14 +220,22 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson()); QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); loop.exec(); QJsonValue result; - if (reply->error() != QNetworkReply::NoError) { + if (!timeout.isActive()) { + qCritical() << "Таймаут запроса (postJson):" << path; + reply->abort(); + result = QJsonValue::Undefined; + } else if (reply->error() != QNetworkReply::NoError) { const QByteArray responseBody = reply->readAll(); qCritical() << "Network error (POST):" << reply->errorString() << path << "\n response body:" << responseBody; @@ -236,8 +259,7 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body } void NetworkService::loginAndSetup() { - QSettings settings("config.ini", QSettings::IniFormat); - const QString apiKey = settings.value("common/api_key").toString(); + const QString apiKey = SettingsDAO::get("api_key"); // Шаг 1: логин QJsonObject loginBody; @@ -257,13 +279,19 @@ void NetworkService::loginAndSetup() { const QJsonObject loginData = loginResult.toObject(); m_accessToken = loginData["access_token"].toString(); - settings.setValue("common/access_token", m_accessToken); - settings.setValue("common/refresh_token", loginData["refresh_token"].toString()); + if (m_accessToken.isEmpty() || !loginData.contains("refresh_token") || !loginData.contains("expires_in")) { + qCritical() << "loginAndSetup: ответ не содержит обязательных полей (access_token/refresh_token/expires_in)"; + emit finishedWithResult(-1); + emit finished(); + return; + } + SettingsDAO::set("access_token", m_accessToken); + SettingsDAO::set("refresh_token", loginData["refresh_token"].toString()); const qint64 expiresIn = static_cast(loginData["expires_in"].toDouble()); - settings.setValue("common/token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); + SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); // Шаг 2: проверяем есть ли desktop_id - const QString desktopId = settings.value("common/desktop_id").toString(); + const QString desktopId = SettingsDAO::get("desktop_id"); if (!desktopId.isEmpty()) { emit finishedWithResult(1); emit finished(); @@ -275,6 +303,7 @@ void NetworkService::loginAndSetup() { desktopBody["blue_token"] = QJsonValue::Null; desktopBody["name"] = QJsonValue::Null; desktopBody["contact"] = QJsonValue::Null; + const QSettings settings("config.ini", QSettings::IniFormat); desktopBody["app_version"] = settings.value("common/version").toString(); const QJsonValue desktopResult = postJson(m_apiBase + m_pathDesktopCreate, desktopBody, true); @@ -285,8 +314,8 @@ void NetworkService::loginAndSetup() { } const QJsonObject desktopData = desktopResult.toObject(); - settings.setValue("common/desktop_id", desktopData["id"].toString()); - settings.setValue("common/profile_id", desktopData["profile_id"].toString()); + SettingsDAO::set("desktop_id", desktopData["id"].toString()); + SettingsDAO::set("profile_id", desktopData["profile_id"].toString()); emit finishedWithResult(1); emit finished(); @@ -350,12 +379,19 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson()); QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); loop.exec(); QJsonValue result; - if (reply->error() != QNetworkReply::NoError) { + if (!timeout.isActive()) { + qCritical() << "Таймаут запроса (PATCH):" << path; + reply->abort(); + } else if (reply->error() != QNetworkReply::NoError) { const QByteArray responseBody = reply->readAll(); qCritical() << "Network error (PATCH):" << reply->errorString() << path << "\n response body:" << responseBody; @@ -487,13 +523,20 @@ void NetworkService::postDeviceScreenshot() { multiPart->setParent(reply); QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); loop.exec(); - if (reply->error() != QNetworkReply::NoError) { + if (!timeout.isActive()) { + qWarning() << "[NetworkService] postDeviceScreenshot timeout for device" << device.apiId; + reply->abort(); + } else if (reply->error() != QNetworkReply::NoError) { qWarning() << "[NetworkService] postDeviceScreenshot failed:" << reply->errorString(); } else { qDebug() << "[NetworkService] postDeviceScreenshot ok for device" << device.apiId; @@ -503,8 +546,7 @@ void NetworkService::postDeviceScreenshot() { } void NetworkService::createDevice() { - const QSettings settings("config.ini", QSettings::IniFormat); - const QString desktopId = settings.value("common/desktop_id").toString(); + const QString desktopId = SettingsDAO::get("desktop_id"); if (desktopId.isEmpty()) { qWarning() << "createDevice: desktop_id not set"; emit finished(); @@ -540,8 +582,7 @@ void NetworkService::updateDevice() { } bool NetworkService::refreshToken() { - QSettings settings("config.ini", QSettings::IniFormat); - const QString storedRefreshToken = settings.value("common/refresh_token").toString(); + const QString storedRefreshToken = SettingsDAO::get("refresh_token"); if (storedRefreshToken.isEmpty()) { qWarning() << "refreshToken: refresh_token отсутствует"; return false; @@ -562,10 +603,14 @@ bool NetworkService::refreshToken() { const QJsonObject data = result.toObject(); m_accessToken = data["access_token"].toString(); - settings.setValue("common/access_token", m_accessToken); - settings.setValue("common/refresh_token", data["refresh_token"].toString()); + if (m_accessToken.isEmpty() || !data.contains("refresh_token") || !data.contains("expires_in")) { + qCritical() << "refreshToken: ответ не содержит обязательных полей"; + return false; + } + SettingsDAO::set("access_token", m_accessToken); + SettingsDAO::set("refresh_token", data["refresh_token"].toString()); const qint64 expiresIn = static_cast(data["expires_in"].toDouble()); - settings.setValue("common/token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); + SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); m_tokenErrorShown = false; // сбрасываем если снова заработало qDebug() << "refreshToken: токен обновлён, истекает через" << expiresIn << "сек"; @@ -573,8 +618,7 @@ bool NetworkService::refreshToken() { } bool NetworkService::ensureValidToken() { - const QSettings settings("config.ini", QSettings::IniFormat); - const qint64 expiresAt = settings.value("common/token_expires_at", 0).toLongLong(); + const qint64 expiresAt = SettingsDAO::getLong("token_expires_at"); const qint64 now = QDateTime::currentSecsSinceEpoch(); // Обновляем заранее — за 30 минут до истечения @@ -588,8 +632,7 @@ bool NetworkService::ensureValidToken() { void NetworkService::scheduleNextRefresh() { if (!m_running) return; - const QSettings settings("config.ini", QSettings::IniFormat); - const qint64 expiresAt = settings.value("common/token_expires_at", 0).toLongLong(); + const qint64 expiresAt = SettingsDAO::getLong("token_expires_at"); const qint64 now = QDateTime::currentSecsSinceEpoch(); // Обновляем за 30 минут до истечения; если уже пора — сразу @@ -621,8 +664,7 @@ void NetworkService::getTasks() { return; } - const QSettings settings("config.ini", QSettings::IniFormat); - const QString accessToken = settings.value("common/access_token").toString(); + const QString accessToken = SettingsDAO::get("access_token"); QUrl url(m_apiBase + m_pathGetTasks); QUrlQuery urlQuery; @@ -702,10 +744,17 @@ void NetworkService::postMonitoringLog(const QString &type, const QString &event multiPart->setParent(reply); QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); loop.exec(); - if (reply->error() != QNetworkReply::NoError) { + if (!timeout.isActive()) { + qWarning() << "[NetworkService] postMonitoringLog timeout"; + reply->abort(); + } else if (reply->error() != QNetworkReply::NoError) { qWarning() << "[NetworkService] postMonitoringLog failed:" << reply->errorString() << "\n response body:" << reply->readAll(); } diff --git a/views/widget/loader/RegistrationWindow.cpp b/views/widget/loader/RegistrationWindow.cpp index 20708d2..1526209 100644 --- a/views/widget/loader/RegistrationWindow.cpp +++ b/views/widget/loader/RegistrationWindow.cpp @@ -10,6 +10,7 @@ #include #include "LoaderDialog.h" +#include "dao/SettingsDAO.h" #include "net/NetworkService.h" RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) { @@ -42,10 +43,7 @@ RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) { return; } - { - QSettings settings("config.ini", QSettings::IniFormat); - settings.setValue("common/api_key", apiKey); - } + SettingsDAO::set("api_key", apiKey); auto *loader = new LoaderDialog(this); loader->show(); @@ -67,8 +65,7 @@ RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) { if (result == 1) { emit onRegisteredSuccess(); } else if (result == 0) { - QSettings s("config.ini", QSettings::IniFormat); - s.remove("common/api_key"); + SettingsDAO::remove("api_key"); QMessageBox::critical(this, "Ошибка входа", "Неверный API ключ. Проверьте и попробуйте снова."); } else { QMessageBox::critical(this, "Ошибка подключения",