1
0
forked from BRT/arc

Add SettingsDAO implementation, schema migration, and related updates

Implemented `SettingsDAO` with `get`, `set`, `remove`, and related methods for key-value storage. Added database schema migration for a new `settings` table. Updated `EventDAO` with error handling improvements and `AbstractDAO` with helper utilities. Enhanced Ozon scripts with ad banner detection and handling.
This commit is contained in:
slava 2026-03-09 15:53:35 +07:00
parent 8eedd07ffd
commit 3d3ed9cbf6
19 changed files with 523 additions and 330 deletions

View File

@ -15,14 +15,10 @@ QString AdbUtils::adbPath() {
#endif #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; QProcess process;
process.start(program, args);
#ifdef Q_OS_WIN
process.start("cmd.exe", {"/C", cmd});
#else
process.start("sh", {"-c", cmd});
#endif
if (!process.waitForFinished() || if (!process.waitForFinished() ||
process.exitStatus() != QProcess::NormalExit || process.exitStatus() != QProcess::NormalExit ||
@ -130,41 +126,34 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool scre
QString output; QString output;
// 1. Dump UI hierarchy // 1. Dump UI hierarchy
QString dumpCmd = QString("%1 -s %2 shell uiautomator dump /sdcard/uidump.xml").arg(adb, deviceId); if (!startProcess(adb, {"-s", deviceId, "shell", "uiautomator", "dump", "/sdcard/uidump.xml"})) {
if (!startProcess(dumpCmd)) {
qWarning() << "Failed to dump UI hierarchy."; qWarning() << "Failed to dump UI hierarchy.";
return {}; return {};
} }
// 2. Optionally pull and save the XML file // 2. Optionally pull and save the XML file
if (screenshot) { if (screenshot) {
QString pullCmd = QString("%1 -s %2 pull /sdcard/uidump.xml step_%3.xml").arg(adb, deviceId).arg(idx); if (!startProcess(adb, {"-s", deviceId, "pull", "/sdcard/uidump.xml",
if (!startProcess(pullCmd)) { QString("step_%1.xml").arg(idx)})) {
qWarning() << "Failed to pull UIDump XML."; qWarning() << "Failed to pull UIDump XML.";
return {}; return {};
} }
} }
// 3. Read the XML content // 3. Read the XML content
QString catCmd = QString("%1 -s %2 shell cat /sdcard/uidump.xml").arg(adb, deviceId);
QString xmlOut; QString xmlOut;
if (!startProcess(catCmd, &xmlOut)) { if (!startProcess(adb, {"-s", deviceId, "shell", "cat", "/sdcard/uidump.xml"}, &xmlOut)) {
qWarning() << "Failed to read UIDump XML."; qWarning() << "Failed to read UIDump XML.";
return {}; return {};
} }
// 4. Remove temp XML file // 4. Remove temp XML file
QString rmCmd = QString("%1 -s %2 shell rm /sdcard/uidump.xml").arg(adb, deviceId); startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/uidump.xml"}); // неважно, успешна ли команда
startProcess(rmCmd); // неважно, успешна ли команда
// 5. Screenshot (if needed) // 5. Screenshot (if needed)
if (screenshot) { if (screenshot) {
QString shotCmds[] = { startProcess(adb, {"-s", deviceId, "shell", "screencap", "-p", "/sdcard/screen.png"});
QString("%1 -s %2 shell screencap -p /sdcard/screen.png").arg(adb, deviceId), startProcess(adb, {"-s", deviceId, "pull", "/sdcard/screen.png",
QString("%1 -s %2 pull /sdcard/screen.png screen_step_%3.png").arg(adb, deviceId).arg(idx), QString("screen_step_%1.png").arg(idx)});
QString("%1 -s %2 shell rm /sdcard/screen.png").arg(adb, deviceId) startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/screen.png"});
};
for (const QString &cmd: shotCmds) {
startProcess(cmd); // ошибки игнорируем для скриншота
}
} }
// 6. Обрезка к началу XML // 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) { bool AdbUtils::takeScreenshot(const QString &deviceId, const QString &name) {
const QString adb = adbPath(); const QString adb = adbPath();
QStringList commands = { struct Step { QStringList args; };
QString("%1 -s %2 shell screencap -p /sdcard/screen.png").arg(adb, deviceId), Step steps[] = {
QString("%1 -s %2 pull /sdcard/screen.png %3.png").arg(adb, deviceId, name), {{"-s", deviceId, "shell", "screencap", "-p", "/sdcard/screen.png"}},
QString("%1 -s %2 shell rm /sdcard/screen.png").arg(adb, deviceId) {{"-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; QString stdErr;
if (!startProcess(cmd, nullptr, &stdErr)) { if (!startProcess(adb, step.args, nullptr, &stdErr)) {
qWarning() << "ADB command failed:" << cmd; qWarning() << "ADB command failed:" << step.args.join(' ');
qWarning() << "Error:" << stdErr; qWarning() << "Error:" << stdErr;
return false; return false;
} }
@ -195,13 +185,8 @@ bool AdbUtils::takeScreenshot(const QString &deviceId, const QString &name) {
} }
QByteArray AdbUtils::captureScreenshotBytes(const QString &deviceId) { QByteArray AdbUtils::captureScreenshotBytes(const QString &deviceId) {
const QString cmd = QString("%1 -s %2 exec-out screencap -p").arg(adbPath(), deviceId);
QProcess process; QProcess process;
#ifdef Q_OS_WIN process.start(adbPath(), {"-s", deviceId, "exec-out", "screencap", "-p"});
process.start("cmd.exe", {"/C", cmd});
#else
process.start("sh", {"-c", cmd});
#endif
if (!process.waitForFinished(10000) || process.exitCode() != 0) { if (!process.waitForFinished(10000) || process.exitCode() != 0) {
qWarning() << "[AdbUtils] captureScreenshotBytes failed for" << deviceId; qWarning() << "[AdbUtils] captureScreenshotBytes failed for" << deviceId;
return {}; return {};
@ -211,17 +196,18 @@ QByteArray AdbUtils::captureScreenshotBytes(const QString &deviceId) {
bool AdbUtils::takeXmlDump(const QString &deviceId, const QString &name) { bool AdbUtils::takeXmlDump(const QString &deviceId, const QString &name) {
const QString adb = adbPath(); const QString adb = adbPath();
QStringList commands = { struct Step { QStringList args; };
QString("%1 -s %2 shell uiautomator dump /sdcard/uidump.xml").arg(adb, deviceId), Step steps[] = {
QString("%1 -s %2 pull /sdcard/uidump.xml %3.xml").arg(adb, deviceId, name), {{"-s", deviceId, "shell", "uiautomator", "dump", "/sdcard/uidump.xml"}},
QString("%1 -s %2 shell cat /sdcard/uidump.xml").arg(adb, deviceId), {{"-s", deviceId, "pull", "/sdcard/uidump.xml", name + ".xml"}},
QString("%1 -s %2 shell rm /sdcard/uidump.xml").arg(adb, deviceId) {{"-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; QString stdErr;
if (!startProcess(cmd, nullptr, &stdErr)) { if (!startProcess(adb, step.args, nullptr, &stdErr)) {
qWarning() << "ADB command failed:" << cmd; qWarning() << "ADB command failed:" << step.args.join(' ');
qWarning() << "Error:" << stdErr; qWarning() << "Error:" << stdErr;
return false; return false;
} }
@ -242,15 +228,16 @@ bool AdbUtils::makeTap(const QString &deviceId, const int x, const int y) {
bool AdbUtils::enableAdbIMEKeyboard(const QString &deviceId) { bool AdbUtils::enableAdbIMEKeyboard(const QString &deviceId) {
const QString adb = adbPath(); const QString adb = adbPath();
QStringList commands = { struct Step { QStringList args; };
QString("%1 -s %2 shell ime enable com.android.adbkeyboard/.AdbIME").arg(adb, deviceId), Step steps[] = {
QString("%1 -s %2 shell ime set com.android.adbkeyboard/.AdbIME").arg(adb, deviceId) {{"-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; QString stdErr;
if (!startProcess(cmd, nullptr, &stdErr)) { if (!startProcess(adb, step.args, nullptr, &stdErr)) {
qWarning() << "ADB command failed:" << cmd; qWarning() << "ADB command failed:" << step.args.join(' ');
qWarning() << "Error:" << stdErr; qWarning() << "Error:" << stdErr;
return false; return false;
} }
@ -275,10 +262,8 @@ bool AdbUtils::inputAdbIMEText(const QString &deviceId, const QString &text) {
bool AdbUtils::disableAdbIMEKeyboard(const QString &deviceId) { bool AdbUtils::disableAdbIMEKeyboard(const QString &deviceId) {
QProcess process; QProcess process;
QString command = QString( process.start(adbPath(), {"-s", deviceId, "shell", "ime", "disable",
"%1 -s %2 shell ime disable com.android.adbkeyboard/.AdbIME" "com.android.adbkeyboard/.AdbIME"});
).arg(adbPath(), deviceId);
process.start(command);
process.waitForFinished(); process.waitForFinished();
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; 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, "-s", deviceId,
"shell", "input", "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(); process.waitForFinished();

View File

@ -47,5 +47,6 @@ public:
static QSet<QString> getListApps(const QString &deviceId); static QSet<QString> getListApps(const QString &deviceId);
private: 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);
}; };

View File

@ -110,6 +110,14 @@ bool CommonScript::runAppAndGoToHomeScreen(
continue; 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 (!findAndInputPincode) {
if (const QList<Node> pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); if (const QList<Node> pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");

View File

@ -10,6 +10,7 @@
#include "dao/AccountDAO.h" #include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h" #include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h" #include "dao/DeviceDAO.h"
#include "dao/SettingsDAO.h"
#include "net/NetworkService.h" #include "net/NetworkService.h"
#include "AppLogger.h" #include "AppLogger.h"
@ -155,7 +156,7 @@ void GetCardInfoScript::doStart() {
// 1. Создаём девайс если нет api_id // 1. Создаём девайс если нет api_id
if (device.apiId.isEmpty()) { if (device.apiId.isEmpty()) {
QJsonObject deviceBody; 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["status"] = "ONLINE";
deviceBody["name"] = device.name; deviceBody["name"] = device.name;
deviceBody["screen_width"] = device.screenWidth; deviceBody["screen_width"] = device.screenWidth;
@ -261,12 +262,18 @@ void GetCardInfoScript::doStart() {
break; break;
} }
// Закрываем баннер "Понятно" если он перекрывает экран // Закрываем баннер "Понятно" и повторно нажимаем "Показать"
Node bannerBtn = xmlScreenParser.findTextNode("Понятно"); Node bannerBtn = xmlScreenParser.findTextNode("Понятно");
if (!bannerBtn.isEmpty()) { if (!bannerBtn.isEmpty()) {
qDebug() << "[GetCardInfo] Banner 'Понятно' detected, closing..."; qDebug() << "[GetCardInfo] Banner 'Понятно' detected, closing...";
AdbUtils::makeTap(m_deviceId, bannerBtn.x(), bannerBtn.y()); 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; continue;
} }

View File

@ -412,22 +412,46 @@ QList<Node> ScreenXmlParser::findPinCodeNodes(const QString &pinCode) {
return result; return result;
} }
// Способ 2 (fallback): анонимные FrameLayout — ищем сетку 4x3 // Способ 2 (fallback): группируем clickable-ноды одинакового размера,
QList<Node> clickable; // ищем сетку 4x3 по паттерну (не привязываясь к координатам экрана)
QMap<QPair<int,int>, QList<Node>> sizeGroups;
for (const Node &node : m_nodes) { for (const Node &node : m_nodes) {
if (node.clickable && node.width() > 0 && node.height() > 0) if (!node.clickable || node.width() <= 0 || node.height() <= 0) continue;
clickable.append(node); 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<Node> candidates; QList<Node> candidates;
for (const Node &n : clickable) { for (auto it = sizeGroups.begin(); it != sizeGroups.end(); ++it) {
if (n.height() <= minH * 3) candidates.append(n); 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) { std::sort(candidates.begin(), candidates.end(), [](const Node &a, const Node &b) {
return a.y() < b.y(); return a.y() < b.y();
}); });
@ -457,7 +481,8 @@ QList<Node> ScreenXmlParser::findPinCodeNodes(const QString &pinCode) {
} }
if (gridRows.size() < 4) { if (gridRows.size() < 4) {
qWarning() << "[findPinCodeNodes] grid not found: gridRows=" << gridRows.size(); qWarning() << "[findPinCodeNodes] grid not found: gridRows=" << gridRows.size()
<< "candidates=" << candidates.size();
return {}; return {};
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

File diff suppressed because one or more lines are too long

View File

@ -66,14 +66,189 @@
focusable="false" focused="false" scrollable="false" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="1" hint=""> bounds="[0,0][1080,2337]" drawing-order="1" hint="">
<node NAF="true" index="0" text="" resource-id="" <node index="0" text="" resource-id=""
class="android.webkit.WebView" class="android.webkit.WebView"
package="ru.ozon.fintech.finance" content-desc="" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="true" checkable="false" checked="false" clickable="false"
enabled="true" focusable="true" focused="false" enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false" password="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]" drawing-order="1" selected="false" bounds="[0,0][1080,2337]" drawing-order="0"
hint=""/> hint="">
<node index="0" text="" resource-id=""
class="android.webkit.WebView"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="true" focused="true"
scrollable="true" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="0" hint="">
<node index="0" text="" resource-id="svelte"
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,241]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="base-go-back-button"
checkable="false" checked="false"
clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[28,144][118,231]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="native-toolbar-close-button"
checkable="false" checked="false"
clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[1036,186][1080,241]"
drawing-order="0" hint=""/>
</node>
<node index="1" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[480,1349][598,1470]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.Image"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[485,1354][593,1462]"
drawing-order="0" hint=""/>
</node>
<node index="2" text=""
resource-id="modal-container"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,2337][1080,2337]"
drawing-order="0" hint=""/>
<node index="3" text=""
resource-id="svelte-announcer"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][5,5]"
drawing-order="0" hint=""/>
</node>
<node index="1" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
<node index="2" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
</node>
<node index="3" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
</node>
</node>
</node>
</node>
</node> </node>
</node> </node>
<node index="1" text="" <node index="1" text=""

View File

@ -2,13 +2,7 @@
list=ozon list=ozon
[common] [common]
access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyODg4MjU2fQ.5WF3EQobCOQpuQr5wcCeYOJW1ER0RJG506WRhEB_Y5E
api_key=0eee77b62013253e02e1a69bc6a505bb37757fb7
currencies=USD, KRW, AZN, RUB currencies=USD, KRW, AZN, RUB
desktop_id=b86eac8e-dc89-4a9e-a5e8-7f85db011391
profile_id=c4552156-d179-4a79-9e91-587038c95cf0
refresh_token=4c78f097-d70b-4bdb-b351-c839fa9eb442
token_expires_at=1772888256
version=0.0.1 version=0.0.1
[db] [db]

View File

@ -6,6 +6,7 @@
#include <QDir> #include <QDir>
#include <QSettings> #include <QSettings>
#include <QThread> #include <QThread>
#include <QRegularExpression>
DatabaseManager::DatabaseManager() = default; DatabaseManager::DatabaseManager() = default;
@ -75,6 +76,13 @@ QString DatabaseManager::connectionName() {
static bool columnExists(QSqlQuery &q, const QString &table, const QString &column) { 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'") q.exec(QString("SELECT COUNT(*) FROM pragma_table_info('%1') WHERE name='%2'")
.arg(table, column)); .arg(table, column));
return q.next() && q.value(0).toInt() > 0; return q.next() && q.value(0).toInt() > 0;
@ -88,7 +96,7 @@ void DatabaseManager::initSchema() {
q.exec("PRAGMA user_version"); q.exec("PRAGMA user_version");
const int version = q.next() ? q.value(0).toInt() : 0; const int version = q.next() ? q.value(0).toInt() : 0;
// ── Версия 1: начальная схема + api_id для devices ──────────────────────── // ── Версия 1: полная схема ─────────────────────────────────────────────────
if (version < 1) { if (version < 1) {
db.transaction(); db.transaction();
@ -117,6 +125,11 @@ void DatabaseManager::initSchema() {
status TEXT DEFAULT 'off', status TEXT DEFAULT 'off',
install TEXT DEFAULT 'no', install TEXT DEFAULT 'no',
comment TEXT, 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, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, code) UNIQUE (device_id, code)
))"); ))");
@ -131,6 +144,8 @@ void DatabaseManager::initSchema() {
currency TEXT DEFAULT '', currency TEXT DEFAULT '',
update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
status TEXT DEFAULT 'active', status TEXT DEFAULT 'active',
card_number TEXT DEFAULT '',
material_id TEXT DEFAULT '',
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, app_code, last_numbers) UNIQUE (device_id, app_code, last_numbers)
))"); ))");
@ -155,185 +170,11 @@ void DatabaseManager::initSchema() {
update_time DATETIME, update_time DATETIME,
complete_time DATETIME, complete_time DATETIME,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
receipt_image BLOB,
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
))"); ))");
q.exec(R"(CREATE TABLE IF NOT EXISTS events ( 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, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER, account_id INTEGER,
external_id TEXT, external_id TEXT,
@ -347,29 +188,38 @@ void DatabaseManager::initSchema() {
update_time DATETIME, update_time DATETIME,
complete_time DATETIME, complete_time DATETIME,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, 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 (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE
))"); ))");
q.exec(R"(INSERT INTO events_new q.exec(R"(CREATE TABLE IF NOT EXISTS banks (
SELECT * FROM events)"); id INTEGER PRIMARY KEY AUTOINCREMENT,
alias TEXT UNIQUE,
name TEXT,
nspk_name TEXT
))");
q.exec("DROP TABLE events"); q.exec(R"(CREATE TABLE IF NOT EXISTS app_logs (
q.exec("ALTER TABLE events_new RENAME TO events"); 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(); db.commit();
q.exec("PRAGMA user_version = 11"); q.exec("PRAGMA user_version = 1");
qDebug() << "[DB] Migration 11 applied"; qDebug() << "[DB] Migration 1 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";
} }
} }

Binary file not shown.

View File

@ -58,7 +58,7 @@ bool AccountDAO::updateAccountById(
query.bindValue(":status", status); query.bindValue(":status", status);
query.bindValue(":description", description); 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(":currency", currency);
query.bindValue(":id", id); query.bindValue(":id", id);

View File

@ -203,7 +203,10 @@ QList<EventInfo> EventDAO::getEvents(int page, int pageSize) {
int EventDAO::getTotalCount() { int EventDAO::getTotalCount() {
QSqlQuery query(DatabaseManager::instance().database()); 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(); if (query.next()) return query.value(0).toInt();
return 0; return 0;
} }

View File

@ -0,0 +1,55 @@
#include "SettingsDAO.h"
#include <QSqlQuery>
#include <QSqlError>
#include <QDebug>
#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));
}

View File

@ -0,0 +1,12 @@
#pragma once
#include <QString>
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);
};

View File

@ -17,6 +17,7 @@
#include <QSettings> #include <QSettings>
#include "AppLogger.h" #include "AppLogger.h"
#include "dao/SettingsDAO.h"
#include "EventHandler.h" #include "EventHandler.h"
#include "net/NetworkService.h" #include "net/NetworkService.h"
#include "ozon/PayByPhoneScript.h" #include "ozon/PayByPhoneScript.h"
@ -196,11 +197,11 @@ int main(int argc, char *argv[]) {
// - неверный api_key [code 0] — показываем окно ввода // - неверный api_key [code 0] — показываем окно ввода
// - всё ок [code 1] — запускаем приложение // - всё ок [code 1] — запускаем приложение
// 2. Если нет api_key — показываем окно для ввода // 2. Если нет api_key — показываем окно для ввода
const QSettings settings("config.ini", QSettings::IniFormat); const QString apiKey = SettingsDAO::get("api_key");
const QString apiKey = settings.value("common/api_key").toString();
auto showRegistration = [&app]() { auto showRegistration = [&app]() {
auto *registrationWindow = new RegistrationWindow(); auto *registrationWindow = new RegistrationWindow();
registrationWindow->setAttribute(Qt::WA_DeleteOnClose);
registrationWindow->show(); registrationWindow->show();
QObject::connect( QObject::connect(
registrationWindow, &RegistrationWindow::onRegisteredSuccess, registrationWindow, registrationWindow, &RegistrationWindow::onRegisteredSuccess, registrationWindow,
@ -219,6 +220,7 @@ int main(int argc, char *argv[]) {
showRegistration(); showRegistration();
} else { } else {
auto *loader = new LoaderWindow(); auto *loader = new LoaderWindow();
loader->setAttribute(Qt::WA_DeleteOnClose);
loader->show(); loader->show();
QObject::connect(loader, &LoaderWindow::loadingFinished, loader, [loader, &app, showRegistration](const int result) { QObject::connect(loader, &LoaderWindow::loadingFinished, loader, [loader, &app, showRegistration](const int result) {
@ -227,10 +229,12 @@ int main(int argc, char *argv[]) {
"Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.", "Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.",
true true
); );
errorWindow->setAttribute(Qt::WA_DeleteOnClose);
errorWindow->show(); errorWindow->show();
QObject::connect(errorWindow, &ErrorWindow::retryRequested, errorWindow, [errorWindow, &app, showRegistration]() { QObject::connect(errorWindow, &ErrorWindow::retryRequested, errorWindow, [errorWindow, &app, showRegistration]() {
errorWindow->close(); errorWindow->close();
auto *newLoader = new LoaderWindow(); auto *newLoader = new LoaderWindow();
newLoader->setAttribute(Qt::WA_DeleteOnClose);
newLoader->show(); newLoader->show();
QObject::connect(newLoader, &LoaderWindow::loadingFinished, newLoader, [newLoader, &app, showRegistration](const int r) { QObject::connect(newLoader, &LoaderWindow::loadingFinished, newLoader, [newLoader, &app, showRegistration](const int r) {
if (r == 1) { if (r == 1) {
@ -241,14 +245,14 @@ int main(int argc, char *argv[]) {
startEventHandler(app); startEventHandler(app);
// startPayByPhoneTest(); // startPayByPhoneTest();
} else if (r == 0) { } else if (r == 0) {
QSettings s("config.ini", QSettings::IniFormat); SettingsDAO::remove("api_key");
s.remove("common/api_key");
showRegistration(); showRegistration();
} else { } else {
auto *err2 = new ErrorWindow( auto *err2 = new ErrorWindow(
"Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.", "Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.",
false false
); );
err2->setAttribute(Qt::WA_DeleteOnClose);
err2->show(); err2->show();
} }
newLoader->close(); newLoader->close();
@ -262,8 +266,7 @@ int main(int argc, char *argv[]) {
startEventHandler(app); startEventHandler(app);
// startPayByPhoneTest(); // startPayByPhoneTest();
} else if (result == 0) { } else if (result == 0) {
QSettings s("config.ini", QSettings::IniFormat); SettingsDAO::remove("api_key");
s.remove("common/api_key");
showRegistration(); showRegistration();
} }
loader->close(); loader->close();

View File

@ -6,6 +6,7 @@
#include <QBuffer> #include <QBuffer>
#include <qeventloop.h> #include <qeventloop.h>
#include <QThread> #include <QThread>
#include <QTimer>
#include <QImage> #include <QImage>
#include <QHttpMultiPart> #include <QHttpMultiPart>
#include <QJsonObject> #include <QJsonObject>
@ -18,6 +19,7 @@
#include "adb/AdbUtils.h" #include "adb/AdbUtils.h"
#include "dao/ApplicationDAO.h" #include "dao/ApplicationDAO.h"
#include "dao/SettingsDAO.h"
DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) { DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) {
const QSettings settings("config.ini", QSettings::IniFormat); const QSettings settings("config.ini", QSettings::IniFormat);
@ -77,14 +79,21 @@ void DeviceScreener::postDevicesData(const QList<DeviceInfo> &devices) {
// локальный цикл — ждём, пока придёт finished() // локальный цикл — ждём, пока придёт finished()
QEventLoop loop; QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QObject::connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { QObject::connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit(); 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(); QByteArray raw = reply->readAll();
QJsonParseError err; QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(raw, &err); QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
@ -103,20 +112,26 @@ void DeviceScreener::postDevicesData(const QList<DeviceInfo> &devices) {
QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body) { QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body) {
if (!m_manager) m_manager = new QNetworkAccessManager(this); if (!m_manager) m_manager = new QNetworkAccessManager(this);
const QSettings settings("config.ini", QSettings::IniFormat);
QNetworkRequest request(QUrl(m_apiBase + path)); QNetworkRequest request(QUrl(m_apiBase + path));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", 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()); QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson());
QEventLoop loop; QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec(); loop.exec();
QJsonValue result; 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()); const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
if (doc.isObject()) { if (doc.isObject()) {
const QJsonObject root = doc.object(); 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) { void DeviceScreener::apiPatch(const QString &path, const QJsonObject &body) {
if (!m_manager) m_manager = new QNetworkAccessManager(this); if (!m_manager) m_manager = new QNetworkAccessManager(this);
const QSettings settings("config.ini", QSettings::IniFormat);
QNetworkRequest request(QUrl(m_apiBase + path)); QNetworkRequest request(QUrl(m_apiBase + path));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", 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()); QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson());
QEventLoop loop; QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec(); 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(); qCritical() << "DeviceScreener apiPatch network error:" << reply->errorString();
} }
reply->deleteLater(); reply->deleteLater();
} }
QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) { QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) {
const QSettings settings("config.ini", QSettings::IniFormat); const QString desktopId = SettingsDAO::get("desktop_id");
const QString desktopId = settings.value("common/desktop_id").toString();
if (desktopId.isEmpty()) { if (desktopId.isEmpty()) {
qWarning() << "createDeviceOnApi: desktop_id не задан"; qWarning() << "createDeviceOnApi: desktop_id не задан";
return {}; return {};
@ -177,11 +197,10 @@ QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) {
void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray &image) { void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray &image) {
if (!m_manager) m_manager = new QNetworkAccessManager(this); if (!m_manager) m_manager = new QNetworkAccessManager(this);
const QSettings settings("config.ini", QSettings::IniFormat);
const QString path = "/api/v1/device/add_screenshot/" + apiId; const QString path = "/api/v1/device/add_screenshot/" + apiId;
QNetworkRequest request(QUrl(m_apiBase + path)); QNetworkRequest request(QUrl(m_apiBase + path));
request.setRawHeader("Authorization", request.setRawHeader("Authorization",
("Bearer " + settings.value("common/access_token").toString()).toUtf8()); ("Bearer " + SettingsDAO::get("access_token")).toUtf8());
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart filePart; QHttpPart filePart;
@ -194,11 +213,18 @@ void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray
QNetworkReply *reply = m_manager->post(request, multiPart); QNetworkReply *reply = m_manager->post(request, multiPart);
multiPart->setParent(reply); multiPart->setParent(reply);
QEventLoop loop; QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec(); 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(); qWarning() << "postScreenshotToApi network error:" << reply->errorString();
} else { } else {
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());

View File

@ -15,6 +15,7 @@
#include "dao/ApplicationDAO.h" #include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h" #include "dao/DeviceDAO.h"
#include "dao/EventDAO.h" #include "dao/EventDAO.h"
#include "dao/SettingsDAO.h"
#include "dao/TransactionDAO.h" #include "dao/TransactionDAO.h"
#include "db/EventInfo.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_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString();
m_manager = new QNetworkAccessManager(this); m_manager = new QNetworkAccessManager(this);
m_token = settings.value("common/token").toString(); m_token = settings.value("common/token").toString();
m_accessToken = settings.value("common/access_token").toString(); m_accessToken = SettingsDAO::get("access_token");
} }
NetworkService::~NetworkService() = default; NetworkService::~NetworkService() = default;
@ -74,14 +75,21 @@ QJsonValue NetworkService::post(QHttpMultiPart *multiPart, const QString &path)
multiPart->setParent(reply); multiPart->setParent(reply);
QEventLoop loop; QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit(); loop.quit();
}); });
loop.exec(); // <- здесь и запускается event-loop потока connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
QJsonValue result; 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(); const QByteArray raw = reply->readAll();
QJsonParseError err; QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
@ -113,14 +121,21 @@ QJsonArray NetworkService::get(const QString &path, const QMap<QString, QString>
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> guard(reply); QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> guard(reply);
QEventLoop loop; QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit(); loop.quit();
}); });
loop.exec(); // <- здесь и запускается event-loop потока connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
QJsonArray result; 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(); const QByteArray raw = reply->readAll();
QJsonParseError err; QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &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()); QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson());
QEventLoop loop; QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit(); loop.quit();
}); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec(); loop.exec();
QJsonValue result; 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(); const QByteArray responseBody = reply->readAll();
qCritical() << "Network error (POST):" << reply->errorString() << path qCritical() << "Network error (POST):" << reply->errorString() << path
<< "\n response body:" << responseBody; << "\n response body:" << responseBody;
@ -236,8 +259,7 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
} }
void NetworkService::loginAndSetup() { void NetworkService::loginAndSetup() {
QSettings settings("config.ini", QSettings::IniFormat); const QString apiKey = SettingsDAO::get("api_key");
const QString apiKey = settings.value("common/api_key").toString();
// Шаг 1: логин // Шаг 1: логин
QJsonObject loginBody; QJsonObject loginBody;
@ -257,13 +279,19 @@ void NetworkService::loginAndSetup() {
const QJsonObject loginData = loginResult.toObject(); const QJsonObject loginData = loginResult.toObject();
m_accessToken = loginData["access_token"].toString(); m_accessToken = loginData["access_token"].toString();
settings.setValue("common/access_token", m_accessToken); if (m_accessToken.isEmpty() || !loginData.contains("refresh_token") || !loginData.contains("expires_in")) {
settings.setValue("common/refresh_token", loginData["refresh_token"].toString()); 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<qint64>(loginData["expires_in"].toDouble()); const qint64 expiresIn = static_cast<qint64>(loginData["expires_in"].toDouble());
settings.setValue("common/token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn);
// Шаг 2: проверяем есть ли desktop_id // Шаг 2: проверяем есть ли desktop_id
const QString desktopId = settings.value("common/desktop_id").toString(); const QString desktopId = SettingsDAO::get("desktop_id");
if (!desktopId.isEmpty()) { if (!desktopId.isEmpty()) {
emit finishedWithResult(1); emit finishedWithResult(1);
emit finished(); emit finished();
@ -275,6 +303,7 @@ void NetworkService::loginAndSetup() {
desktopBody["blue_token"] = QJsonValue::Null; desktopBody["blue_token"] = QJsonValue::Null;
desktopBody["name"] = QJsonValue::Null; desktopBody["name"] = QJsonValue::Null;
desktopBody["contact"] = QJsonValue::Null; desktopBody["contact"] = QJsonValue::Null;
const QSettings settings("config.ini", QSettings::IniFormat);
desktopBody["app_version"] = settings.value("common/version").toString(); desktopBody["app_version"] = settings.value("common/version").toString();
const QJsonValue desktopResult = postJson(m_apiBase + m_pathDesktopCreate, desktopBody, true); const QJsonValue desktopResult = postJson(m_apiBase + m_pathDesktopCreate, desktopBody, true);
@ -285,8 +314,8 @@ void NetworkService::loginAndSetup() {
} }
const QJsonObject desktopData = desktopResult.toObject(); const QJsonObject desktopData = desktopResult.toObject();
settings.setValue("common/desktop_id", desktopData["id"].toString()); SettingsDAO::set("desktop_id", desktopData["id"].toString());
settings.setValue("common/profile_id", desktopData["profile_id"].toString()); SettingsDAO::set("profile_id", desktopData["profile_id"].toString());
emit finishedWithResult(1); emit finishedWithResult(1);
emit finished(); 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()); QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson());
QEventLoop loop; QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec(); loop.exec();
QJsonValue result; 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(); const QByteArray responseBody = reply->readAll();
qCritical() << "Network error (PATCH):" << reply->errorString() << path qCritical() << "Network error (PATCH):" << reply->errorString() << path
<< "\n response body:" << responseBody; << "\n response body:" << responseBody;
@ -487,13 +523,20 @@ void NetworkService::postDeviceScreenshot() {
multiPart->setParent(reply); multiPart->setParent(reply);
QEventLoop loop; QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit(); loop.quit();
}); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec(); 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(); qWarning() << "[NetworkService] postDeviceScreenshot failed:" << reply->errorString();
} else { } else {
qDebug() << "[NetworkService] postDeviceScreenshot ok for device" << device.apiId; qDebug() << "[NetworkService] postDeviceScreenshot ok for device" << device.apiId;
@ -503,8 +546,7 @@ void NetworkService::postDeviceScreenshot() {
} }
void NetworkService::createDevice() { void NetworkService::createDevice() {
const QSettings settings("config.ini", QSettings::IniFormat); const QString desktopId = SettingsDAO::get("desktop_id");
const QString desktopId = settings.value("common/desktop_id").toString();
if (desktopId.isEmpty()) { if (desktopId.isEmpty()) {
qWarning() << "createDevice: desktop_id not set"; qWarning() << "createDevice: desktop_id not set";
emit finished(); emit finished();
@ -540,8 +582,7 @@ void NetworkService::updateDevice() {
} }
bool NetworkService::refreshToken() { bool NetworkService::refreshToken() {
QSettings settings("config.ini", QSettings::IniFormat); const QString storedRefreshToken = SettingsDAO::get("refresh_token");
const QString storedRefreshToken = settings.value("common/refresh_token").toString();
if (storedRefreshToken.isEmpty()) { if (storedRefreshToken.isEmpty()) {
qWarning() << "refreshToken: refresh_token отсутствует"; qWarning() << "refreshToken: refresh_token отсутствует";
return false; return false;
@ -562,10 +603,14 @@ bool NetworkService::refreshToken() {
const QJsonObject data = result.toObject(); const QJsonObject data = result.toObject();
m_accessToken = data["access_token"].toString(); m_accessToken = data["access_token"].toString();
settings.setValue("common/access_token", m_accessToken); if (m_accessToken.isEmpty() || !data.contains("refresh_token") || !data.contains("expires_in")) {
settings.setValue("common/refresh_token", data["refresh_token"].toString()); qCritical() << "refreshToken: ответ не содержит обязательных полей";
return false;
}
SettingsDAO::set("access_token", m_accessToken);
SettingsDAO::set("refresh_token", data["refresh_token"].toString());
const qint64 expiresIn = static_cast<qint64>(data["expires_in"].toDouble()); const qint64 expiresIn = static_cast<qint64>(data["expires_in"].toDouble());
settings.setValue("common/token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn);
m_tokenErrorShown = false; // сбрасываем если снова заработало m_tokenErrorShown = false; // сбрасываем если снова заработало
qDebug() << "refreshToken: токен обновлён, истекает через" << expiresIn << "сек"; qDebug() << "refreshToken: токен обновлён, истекает через" << expiresIn << "сек";
@ -573,8 +618,7 @@ bool NetworkService::refreshToken() {
} }
bool NetworkService::ensureValidToken() { bool NetworkService::ensureValidToken() {
const QSettings settings("config.ini", QSettings::IniFormat); const qint64 expiresAt = SettingsDAO::getLong("token_expires_at");
const qint64 expiresAt = settings.value("common/token_expires_at", 0).toLongLong();
const qint64 now = QDateTime::currentSecsSinceEpoch(); const qint64 now = QDateTime::currentSecsSinceEpoch();
// Обновляем заранее — за 30 минут до истечения // Обновляем заранее — за 30 минут до истечения
@ -588,8 +632,7 @@ bool NetworkService::ensureValidToken() {
void NetworkService::scheduleNextRefresh() { void NetworkService::scheduleNextRefresh() {
if (!m_running) return; if (!m_running) return;
const QSettings settings("config.ini", QSettings::IniFormat); const qint64 expiresAt = SettingsDAO::getLong("token_expires_at");
const qint64 expiresAt = settings.value("common/token_expires_at", 0).toLongLong();
const qint64 now = QDateTime::currentSecsSinceEpoch(); const qint64 now = QDateTime::currentSecsSinceEpoch();
// Обновляем за 30 минут до истечения; если уже пора — сразу // Обновляем за 30 минут до истечения; если уже пора — сразу
@ -621,8 +664,7 @@ void NetworkService::getTasks() {
return; return;
} }
const QSettings settings("config.ini", QSettings::IniFormat); const QString accessToken = SettingsDAO::get("access_token");
const QString accessToken = settings.value("common/access_token").toString();
QUrl url(m_apiBase + m_pathGetTasks); QUrl url(m_apiBase + m_pathGetTasks);
QUrlQuery urlQuery; QUrlQuery urlQuery;
@ -702,10 +744,17 @@ void NetworkService::postMonitoringLog(const QString &type, const QString &event
multiPart->setParent(reply); multiPart->setParent(reply);
QEventLoop loop; QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec(); 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() qWarning() << "[NetworkService] postMonitoringLog failed:" << reply->errorString()
<< "\n response body:" << reply->readAll(); << "\n response body:" << reply->readAll();
} }

View File

@ -10,6 +10,7 @@
#include <QSettings> #include <QSettings>
#include "LoaderDialog.h" #include "LoaderDialog.h"
#include "dao/SettingsDAO.h"
#include "net/NetworkService.h" #include "net/NetworkService.h"
RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) { RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) {
@ -42,10 +43,7 @@ RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) {
return; return;
} }
{ SettingsDAO::set("api_key", apiKey);
QSettings settings("config.ini", QSettings::IniFormat);
settings.setValue("common/api_key", apiKey);
}
auto *loader = new LoaderDialog(this); auto *loader = new LoaderDialog(this);
loader->show(); loader->show();
@ -67,8 +65,7 @@ RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) {
if (result == 1) { if (result == 1) {
emit onRegisteredSuccess(); emit onRegisteredSuccess();
} else if (result == 0) { } else if (result == 0) {
QSettings s("config.ini", QSettings::IniFormat); SettingsDAO::remove("api_key");
s.remove("common/api_key");
QMessageBox::critical(this, "Ошибка входа", "Неверный API ключ. Проверьте и попробуйте снова."); QMessageBox::critical(this, "Ошибка входа", "Неверный API ключ. Проверьте и попробуйте снова.");
} else { } else {
QMessageBox::critical(this, "Ошибка подключения", QMessageBox::critical(this, "Ошибка подключения",