diff --git a/CMakeLists.txt b/CMakeLists.txt index ae1eaa4..357f34a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,8 +27,7 @@ if (WIN32) elseif (APPLE) # Укажи путь к Qt6 set(CMAKE_PREFIX_PATH "/opt/homebrew/opt/qt") - include_directories("/opt/homebrew/opt/tesseract/include") - link_directories("/opt/homebrew/opt/tesseract/lib") + link_directories("/opt/homebrew/lib") endif () find_package(Qt6 REQUIRED COMPONENTS @@ -136,7 +135,6 @@ elseif (APPLE) Qt6::Sql Qt6::Xml Qt6::Network - tesseract ) endif () diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp new file mode 100644 index 0000000..7e070c3 --- /dev/null +++ b/android/ozon/GetCardInfoScript.cpp @@ -0,0 +1,309 @@ +#include "GetCardInfoScript.h" + +#include +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/AccountDAO.h" +#include "dao/ApplicationDAO.h" +#include "dao/DeviceDAO.h" +#include "net/NetworkService.h" + +namespace Ozon { + +GetCardInfoScript::GetCardInfoScript( + QString deviceId, + QString appCode, + QString lastNumbers, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)), + m_lastNumbers(std::move(lastNumbers)) { +} + +GetCardInfoScript::~GetCardInfoScript() = default; + +void GetCardInfoScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + // Читаем текущий экран + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Закрываем рекламный bottom sheet, если открыт + if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + qDebug() << "[GetCardInfo] Ad banner detected, closing..."; + AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Свежий дамп перед проверкой домашнего экрана + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Если не на домашнем экране — перезапускаем приложение + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[GetCardInfo] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Закрываем баннеры на главной + tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Переходим в профиль и парсим имя и телефон (только если ещё нет в БД) + const bool hasProfileInfo = !app.fullName.isEmpty() && !app.phone.isEmpty(); + Node userNameNode; + if (!hasProfileInfo) { + userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight); + } + if (!hasProfileInfo && !userNameNode.isEmpty()) { + qDebug() << "[GetCardInfo] Navigating to profile to parse name/phone..."; + AdbUtils::makeTap(m_deviceId, userNameNode.x(), userNameNode.y()); + QThread::msleep(2500); + + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (xmlScreenParser.isProfileScreen()) break; + QThread::msleep(1000); + } + + if (xmlScreenParser.isProfileScreen()) { + const QString fullName = xmlScreenParser.parseProfileFullName(); + const QString phone = xmlScreenParser.parseProfilePhone(); + qDebug() << "[GetCardInfo] Profile parsed - fullName:" << fullName << "phone:" << phone; + ApplicationDAO::updateProfileInfo(app.id, fullName, phone); + } else { + qWarning() << "[GetCardInfo] Profile screen not reached, skipping profile parse"; + } + + AdbUtils::goBack(m_deviceId); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } else { + qDebug() << "[GetCardInfo] Profile info already in DB or username not found, skipping profile parse"; + } + + // Ждём пока все карты подгрузятся — до 5 попыток + QList cards; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + cards = xmlScreenParser.parseCardsOnHomeScreen(); + if (!cards.isEmpty()) break; + qDebug() << "[GetCardInfo] No cards found on home screen, attempt" << attempt + 1 << "/ 5"; + QThread::msleep(1500); + } + + if (cards.isEmpty()) { + m_error = "No cards found on home screen: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + qDebug() << "[GetCardInfo] Found" << cards.size() << "card(s) on home screen"; + + // Апсёртим аккаунты для всех найденных карт + for (AccountInfo &card : cards) { + card.deviceId = m_deviceId; + card.appCode = m_appCode; + card.updateTime = QDateTime::currentDateTimeUtc(); + card.status = AccountStatus::Active; + if (!AccountDAO::upsertAccount(card)) { + qWarning() << "[GetCardInfo] Failed to upsert account for card" << card.lastNumbers; + } else { + qDebug() << "[GetCardInfo] Upserted account:" << card.lastNumbers + << card.description << card.amount; + } + } + + // Сохраняем баланс домашнего экрана в application + const double totalAmount = cards.isEmpty() ? 0.0 : cards[0].amount; + if (totalAmount > 0.0) { + ApplicationDAO::updateAmount(app.id, totalAmount); + } + + // Синхронная отправка на сервер (NetworkService блокируется через QEventLoop внутри) + NetworkService ns; + ns.setDeviceId(m_deviceId); + + // 1. Создаём девайс если нет api_id + if (device.apiId.isEmpty()) { + QJsonObject deviceBody; + deviceBody["desktop_id"] = QSettings("config.ini", QSettings::IniFormat).value("common/desktop_id").toString(); + deviceBody["status"] = "ONLINE"; + deviceBody["name"] = device.name; + deviceBody["screen_width"] = device.screenWidth; + deviceBody["screen_height"]= device.screenHeight; + deviceBody["battery"] = device.battery; + ns.setPayload(QJsonDocument(deviceBody).toJson()); + ns.createDevice(); + qDebug() << "[GetCardInfo] createDevice done"; + } + + // 2. Отправляем банковский профиль (используем свежие данные из БД — имя/телефон уже спарсены) + { + const ApplicationInfo freshApp = ApplicationDAO::getApplication(m_deviceId, m_appCode); + const QStringList parts = freshApp.fullName.split(' ', Qt::SkipEmptyParts); + + // ISO 4217 numeric currency codes + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944} + }; + const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643); + + QJsonObject profileBody; + profileBody["first_name"] = parts.value(0); + profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : ""; + profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); + profileBody["bank_name"] = m_appCode; + // Телефон: убираем пробелы, оставляем "+" + const QString phone = freshApp.phone.simplified().remove(' '); + profileBody["phone_number"] = phone; + profileBody["balance"] = QString::number(qRound64(totalAmount * 100)); + profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled"; + profileBody["currency"] = currencyCode; + ns.setPayload(QJsonDocument(profileBody).toJson()); + + if (freshApp.bankProfileId.isEmpty()) { + ns.addExtra("app_id", freshApp.id); + ns.postBankProfile(); + qDebug() << "[GetCardInfo] postBankProfile done"; + } else { + ns.addExtra("bank_profile_id", freshApp.bankProfileId); + ns.patchBankProfile(); + qDebug() << "[GetCardInfo] patchBankProfile done"; + } + } + + // Для каждой карты переходим на её экран и получаем полный номер через OCR + for (int i = 0; i < cards.size(); ++i) { + const AccountInfo &card = cards[i]; + + // Если передан конкретный lastNumbers — обрабатываем только его + if (!m_lastNumbers.isEmpty() && card.lastNumbers != m_lastNumbers) continue; + + qDebug() << "[GetCardInfo] Getting card number for" << card.lastNumbers; + + // Если номер карты уже есть в БД — сумма уже обновлена через upsert, навигация не нужна + AccountInfo existing = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers); + if (existing.id != -1 && !existing.cardNumber.isEmpty()) { + qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "already has number, skipping"; + continue; + } + + // Тапаем по кнопке конкретной карты — каждая открывает свой экран + Node cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers); + if (cardBtn.isEmpty()) { + qWarning() << "[GetCardInfo] Card button not found for" << card.lastNumbers; + continue; + } + + AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); + + // Ждём экран карты — ищем "Реквизиты карты" в XML дампе + Node showBtn; + for (int attempt = 0; attempt < 10; ++attempt) { + QThread::msleep(2000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node reqNode = xmlScreenParser.findTextNode("Реквизиты карты"); + if (!reqNode.isEmpty()) { + showBtn = xmlScreenParser.findButtonNode("Показать", false); + if (!showBtn.isEmpty()) break; + } + qDebug() << "[GetCardInfo] Card screen not loaded, attempt" << attempt + 1 << "/ 10"; + } + + if (showBtn.isEmpty()) { + qWarning() << "[GetCardInfo] 'Показать' not found for card" << card.lastNumbers; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + continue; + } + + qDebug() << "[GetCardInfo] 'Показать' at" << showBtn.x() << showBtn.y(); + AdbUtils::makeTap(m_deviceId, showBtn.x(), showBtn.y()); + + // Ждём появления полного номера карты в XML дампе + QString cardNumber; + for (int attempt = 0; attempt < 10; ++attempt) { + QThread::msleep(2000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + cardNumber = xmlScreenParser.findFullCardNumber(); + if (!cardNumber.isEmpty()) { + qDebug() << "[GetCardInfo] Card number found on attempt" << attempt + 1; + break; + } + qDebug() << "[GetCardInfo] Card number not visible, attempt" << attempt + 1 << "/ 10"; + } + + if (!cardNumber.isEmpty()) { + qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "full number:" << cardNumber; + // Получаем id аккаунта из БД и сохраняем номер карты + AccountInfo saved = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers); + if (saved.id != -1) { + AccountDAO::updateCardNumber(saved.id, cardNumber); + } + } else { + qWarning() << "[GetCardInfo] Card number not found for" << card.lastNumbers; + } + + // Возвращаемся на домашний экран для следующей карты + if (i < cards.size() - 1) { + AdbUtils::goBack(m_deviceId); + QThread::msleep(2000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + } + + // 3. Отправляем материалы (карты) — после OCR, когда номера карт уже в БД + const ApplicationInfo updatedApp = ApplicationDAO::getApplication(m_deviceId, m_appCode); + if (!updatedApp.bankProfileId.isEmpty()) { + for (const AccountInfo &card : cards) { + const AccountInfo dbCard = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers); + if (dbCard.cardNumber.isEmpty()) continue; + if (!dbCard.materialId.isEmpty()) { + qDebug() << "[GetCardInfo] material already exists for card" << dbCard.lastNumbers << ", skipping"; + continue; + } + + QJsonObject materialBody; + materialBody["bank_profile_id"] = updatedApp.bankProfileId; + materialBody["card_number"] = dbCard.cardNumber; + materialBody["name"] = updatedApp.fullName; + ns.setPayload(QJsonDocument(materialBody).toJson()); + ns.addExtra("account_id", dbCard.id); + ns.postMaterial(); + qDebug() << "[GetCardInfo] postMaterial done for card" << dbCard.lastNumbers; + } + } else { + qWarning() << "[GetCardInfo] bankProfileId empty, skipping materials"; + } + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); +} + +} // namespace Ozon diff --git a/android/ozon/GetCardInfoScript.h b/android/ozon/GetCardInfoScript.h new file mode 100644 index 0000000..1d4deda --- /dev/null +++ b/android/ozon/GetCardInfoScript.h @@ -0,0 +1,32 @@ +#pragma once + +#include "CommonScript.h" + +namespace Ozon { + +class GetCardInfoScript final : public CommonScript { + Q_OBJECT + +public: + // lastNumbers — последние 4 цифры карты (например "9865"). + // Если пусто — берём первую найденную карту на домашнем экране. + explicit GetCardInfoScript( + QString deviceId, + QString appCode, + QString lastNumbers, + QObject *parent = nullptr + ); + + ~GetCardInfoScript() override; + +protected: + void doStart() override; + +private: + const QString m_deviceId; + const QString m_appCode; + const QString m_lastNumbers; + QString m_error; +}; + +} // namespace Ozon diff --git a/android/ozon/GetLastTransactionsScript.cpp b/android/ozon/GetLastTransactionsScript.cpp new file mode 100644 index 0000000..398ecee --- /dev/null +++ b/android/ozon/GetLastTransactionsScript.cpp @@ -0,0 +1,92 @@ +#include "GetLastTransactionsScript.h" + +#include + +#include "adb/AdbUtils.h" +#include "dao/ApplicationDAO.h" +#include "dao/DeviceDAO.h" + +namespace Ozon { + +GetLastTransactionsScript::GetLastTransactionsScript( + QString deviceId, + QString appCode, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)) { +} + +GetLastTransactionsScript::~GetLastTransactionsScript() = default; + +void GetLastTransactionsScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Закрываем рекламный bottom sheet, если открыт + if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + qDebug() << "[GetLastTransactions] Ad banner detected, closing..."; + AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Если не на домашнем экране — перезапускаем приложение + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[GetLastTransactions] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Закрываем баннеры на главной + tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Скроллим вниз до кнопки "Все" и тапаем по ней + Node allBtn = swipeToButton(m_deviceId, "Все", m_counter, + device.screenWidth, device.screenHeight); + + if (allBtn.isEmpty()) { + m_error = "Button 'Все' not found after scrolling: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + qDebug() << "[GetLastTransactions] Found 'Все' at" << allBtn.x() << allBtn.y(); + AdbUtils::makeTap(m_deviceId, allBtn.x(), allBtn.y()); + QThread::msleep(2000); + + // Парсим транзакции за сегодня и вчера + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + QList transactions = xmlScreenParser.parseTodayAndYesterdayHistory(); + + qDebug() << "[GetLastTransactions] Found" << transactions.size() << "transactions"; + for (const TransactionInfo &tx : transactions) { + qDebug() << "[GetLastTransactions]" << tx.description << tx.amount << tx.bankTime; + } + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); +} + +} // namespace Ozon diff --git a/android/ozon/GetLastTransactionsScript.h b/android/ozon/GetLastTransactionsScript.h new file mode 100644 index 0000000..9a37c9d --- /dev/null +++ b/android/ozon/GetLastTransactionsScript.h @@ -0,0 +1,27 @@ +#pragma once +#include "CommonScript.h" + +namespace Ozon { + +class GetLastTransactionsScript final : public CommonScript { + Q_OBJECT + +public: + explicit GetLastTransactionsScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr + ); + + ~GetLastTransactionsScript() override; + +protected: + void doStart() override; + +private: + const QString m_deviceId; + const QString m_appCode; + QString m_error; +}; + +} // namespace Ozon diff --git a/android/ozon/GetProfileInfoScript.cpp b/android/ozon/GetProfileInfoScript.cpp new file mode 100644 index 0000000..59da1c5 --- /dev/null +++ b/android/ozon/GetProfileInfoScript.cpp @@ -0,0 +1,149 @@ +#include "GetProfileInfoScript.h" + +#include +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/ApplicationDAO.h" +#include "dao/DeviceDAO.h" +#include "net/NetworkService.h" + +namespace Ozon { + +GetProfileInfoScript::GetProfileInfoScript( + QString deviceId, + QString appCode, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)) { +} + +GetProfileInfoScript::~GetProfileInfoScript() = default; + +void GetProfileInfoScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + // Читаем текущий экран + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Закрываем рекламный bottom sheet, если открыт (home_ad_1) + if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + qDebug() << "[GetProfileInfo] Ad banner detected, closing..."; + AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Свежий дамп перед проверкой домашнего экрана + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Если мы не на домашнем экране — перезапускаем приложение и вводим пин-код + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[GetProfileInfo] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + // После runAppAndGoToHomeScreen xml уже обновлён внутри метода, + // но делаем свежий дамп, чтобы убедиться + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Закрываем баннеры на главной, если есть + tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Находим имя пользователя в верхней части домашнего экрана и тапаем по нему + Node userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight); + if (userNameNode.isEmpty()) { + m_error = "Cannot find user name on home screen: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + qDebug() << "[GetProfileInfo] Tapping on user name:" << userNameNode.text + << "at" << userNameNode.x() << userNameNode.y(); + AdbUtils::makeTap(m_deviceId, userNameNode.x(), userNameNode.y()); + + // Ждём загрузки страницы профиля + QThread::msleep(2500); + + // Читаем XML профиля и проверяем что мы действительно там + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (xmlScreenParser.isProfileScreen()) { + break; + } + QThread::msleep(1000); + } + + if (!xmlScreenParser.isProfileScreen()) { + m_error = "Profile screen not detected after tap: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // Извлекаем полное имя и телефон + const QString fullName = xmlScreenParser.parseProfileFullName(); + const QString phone = xmlScreenParser.parseProfilePhone().simplified().remove(' '); + + qDebug() << "[GetProfileInfo] fullName:" << fullName << "phone:" << phone; + + if (!ApplicationDAO::updateProfileInfo(app.id, fullName, phone)) { + m_error = "Failed to save profile info to DB: " + m_deviceId; + qWarning() << m_error; + } + + // Отправляем bank_profile на сервер если профиля ещё нет + if (app.bankProfileId.isEmpty()) { + const ApplicationInfo freshApp = ApplicationDAO::getApplication(m_deviceId, m_appCode); + + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944} + }; + const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643); + + const QStringList parts = fullName.split(' ', Qt::SkipEmptyParts); + QJsonObject profileBody; + profileBody["first_name"] = parts.value(0); + profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : ""; + profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); + profileBody["bank_name"] = m_appCode; + profileBody["phone_number"] = phone; + profileBody["balance"] = QString::number(qRound64(freshApp.amount * 100)); + profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled"; + profileBody["currency"] = currencyCode; + + NetworkService ns; + ns.setDeviceId(m_deviceId); + ns.setPayload(QJsonDocument(profileBody).toJson()); + ns.addExtra("app_id", freshApp.id); + ns.postBankProfile(); + qDebug() << "[GetProfileInfo] postBankProfile done"; + } + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); +} + +} // namespace Ozon diff --git a/android/ozon/GetProfileInfoScript.h b/android/ozon/GetProfileInfoScript.h new file mode 100644 index 0000000..2b24158 --- /dev/null +++ b/android/ozon/GetProfileInfoScript.h @@ -0,0 +1,27 @@ +#pragma once +#include "CommonScript.h" + +namespace Ozon { + +class GetProfileInfoScript final : public CommonScript { + Q_OBJECT + +public: + ~GetProfileInfoScript() override; + + explicit GetProfileInfoScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + const QString m_deviceId; + const QString m_appCode; + QString m_error; +}; + +} // namespace Ozon diff --git a/android/ozon/LoginAndCheckAccountsScript.cpp b/android/ozon/LoginAndCheckAccountsScript.cpp index fc3206d..5e5563c 100644 --- a/android/ozon/LoginAndCheckAccountsScript.cpp +++ b/android/ozon/LoginAndCheckAccountsScript.cpp @@ -1,9 +1,12 @@ #include "LoginAndCheckAccountsScript.h" +#include + #include "adb/AdbUtils.h" #include "dao/AccountDAO.h" #include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" +#include "GetCardInfoScript.h" namespace Ozon { @@ -60,9 +63,12 @@ void LoginAndCheckAccountsScript::doStart() { } else { qWarning() << "Cant go to all products: " << m_deviceId << " " << m_appCode; } + + // Запускаем сканирование баланса, данных карт и отправку на сервер (синхронно) + GetCardInfoScript cardScript(m_deviceId, m_appCode, ""); + cardScript.start(); } - AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); } diff --git a/android/ozon/ScreenXmlParser.cpp b/android/ozon/ScreenXmlParser.cpp index 1acd7de..197373e 100644 --- a/android/ozon/ScreenXmlParser.cpp +++ b/android/ozon/ScreenXmlParser.cpp @@ -179,6 +179,181 @@ bool ScreenXmlParser::isHomeScreen() { return false; } +Node ScreenXmlParser::tryToFindAdBanner() { + // Рекламный bottom sheet (home_ad_1) определяется по наличию кликабельного + // фонового оверлея "touch_outside" и самого bottom sheet контейнера. + bool hasBottomSheet = false; + Node touchOutside; + for (const Node &node: m_nodes) { + if (node.resourceId == "ru.ozon.fintech.finance:id/design_bottom_sheet") { + hasBottomSheet = true; + } + if (node.resourceId == "ru.ozon.fintech.finance:id/touch_outside" && node.clickable) { + touchOutside = node; + } + } + if (hasBottomSheet && !touchOutside.isEmpty()) { + return touchOutside; + } + return {}; +} + +bool ScreenXmlParser::isProfileScreen() { + // Страница профиля Ozon: есть заголовок "Аккаунт" и кнопка "Выйти" + bool hasTitle = false; + bool hasExit = false; + for (const Node &node: m_nodes) { + const QString t = node.text.trimmed(); + if (t == "Аккаунт") hasTitle = true; + if (t == "Выйти") hasExit = true; + if (hasTitle && hasExit) return true; + } + return false; +} + +Node ScreenXmlParser::findUserNameOnHomeScreen(const int screenHeight) { + // Имя пользователя — TextView в верхней части домашнего экрана (y < screenHeight/5). + // Тап по его координатам делегируется родительскому кликабельному View. + static const QSet knownLabels = { + "Финансы", "Платежи", "Выгода", "Чат", "Ещё" + }; + for (const Node &node: m_nodes) { + if (node.className == "android.widget.TextView" + && !node.text.trimmed().isEmpty() + && node.y() > 0 + && node.y() < screenHeight / 5 + && !knownLabels.contains(node.text.trimmed())) { + return node; + } + } + return {}; +} + +QString ScreenXmlParser::parseProfileFullName() { + // Полное имя — TextView ниже зоны заголовка, выше строки с телефоном. + // В profile.xml это единственный короткий нейтральный TextView в диапазоне y ≈ 400–750. + static const QSet uiLabels = { + "Аккаунт", "Мои документы", "Паспорт и ИНН", + "Уровень счёта Лимиты и тарифы", "Повысить", + "Изменить код-пароль", "Изменить кодовое слово", + "Использовать данные биометрии", "Выйти", + "Оформите подписку", + "QR-код для пополнений Принимайте чаевые и донаты", + "Добавить Почта" + }; + for (const Node &node: m_nodes) { + if (node.className == "android.widget.TextView" + && !node.text.trimmed().isEmpty() + && node.y() > 400 + && node.y() < 750 + && !uiLabels.contains(node.text.trimmed())) { + return node.text.trimmed(); + } + } + return {}; +} + +Node ScreenXmlParser::findCardButtonOnHomeScreen() { + // Карточка на домашнем экране — Button, текст которого заканчивается на 4 цифры. + // Формат может быть "9865", "•• 9865", "**** 9865" и т.п. + static const QRegularExpression endsWithFourDigits(R"(\d{4}\s*$)"); + for (const Node &node : m_nodes) { + if (node.className == "android.widget.Button" + && node.clickable + && endsWithFourDigits.match(node.text.trimmed()).hasMatch()) { + qDebug() << "[findCardButton] Found:" << node.text << "at" << node.x() << node.y(); + return node; + } + } + // Диагностика: выводим все clickable Button'ы чтобы понять что есть на экране + qDebug() << "[findCardButton] Not found. All clickable Buttons:"; + for (const Node &node : m_nodes) { + if (node.className == "android.widget.Button" && node.clickable) { + qDebug() << " text=" << node.text << "resId=" << node.resourceId; + } + } + return {}; +} + +Node ScreenXmlParser::findCardButtonByLastNumbers(const QString &lastNumbers) { + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button" || !node.clickable) continue; + if (node.text.trimmed().endsWith(lastNumbers)) { + qDebug() << "[findCardBtn] Found by lastNumbers" << lastNumbers + << "text=" << node.text << "at" << node.x() << node.y(); + return node; + } + } + qWarning() << "[findCardBtn] Not found for lastNumbers=" << lastNumbers + << ". Clickable buttons:"; + for (const Node &node : m_nodes) { + if (node.className == "android.widget.Button" && node.clickable) + qWarning() << " text=" << node.text; + } + return {}; +} + +QList ScreenXmlParser::parseCardsOnHomeScreen() { + // "Основной счёт X ₽" — общая кнопка с балансом для всех карт. + // Карты — отдельные кнопки с текстом из 4 цифр (могут быть любые non-breaking spaces). + // Сумма из "Основной счёт" проставляется на все карты. + + static const QRegularExpression endsWithFourDigits(R"(\d{4}\s*$)"); + + // 1. Ищем кнопку "Основной счёт X ₽" — содержит "Основной" и "₽" + double commonAmount = 0.0; + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button") continue; + QString t = node.text; + t.replace(QChar(0x00A0), ' ').replace(QChar(0x202F), ' '); + if (!t.contains("Основной", Qt::CaseInsensitive) || !t.contains("₽")) continue; + qDebug() << "[parseCards] Found balance button, text:" << t; + const QRegularExpressionMatch m = QRegularExpression( + R"(([\d\s]+(?:[.,]\d+)?)\s*₽)" + ).match(t); + if (m.hasMatch()) { + QString amtStr = m.captured(1).remove(' ').replace(',', '.'); + commonAmount = amtStr.toDouble(); + qDebug() << "[parseCards] Parsed amount:" << commonAmount; + break; + } + qWarning() << "[parseCards] Amount regex not matched in:" << t; + } + if (commonAmount == 0.0) + qWarning() << "[parseCards] Balance button not found or amount=0"; + + // 2. Собираем все кнопки карт (текст заканчивается на 4 цифры) + QList cards; + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button" || !node.clickable) continue; + QString t = node.text.trimmed(); + if (!endsWithFourDigits.match(t).hasMatch()) continue; + + AccountInfo acc; + acc.lastNumbers = t.right(4); + acc.amount = commonAmount; + acc.currency = "RUB"; + acc.description = "Виртуальная карта"; + qDebug() << "[parseCards] Card found: lastNumbers=" << acc.lastNumbers + << "text=" << node.text << "amount=" << commonAmount; + cards.append(acc); + } + + qDebug() << "[parseCards] Total cards found:" << cards.size(); + return cards; +} + +QString ScreenXmlParser::parseProfilePhone() { + // Телефон — Button с текстом вида "+7 953 803 83 18 Номер телефона" + for (const Node &node: m_nodes) { + if (node.text.contains("Номер телефона")) { + const int idx = node.text.indexOf("Номер телефона"); + return node.text.left(idx).trimmed(); + } + } + return {}; +} + QList ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { // Ozon PIN кнопки — анонимные FrameLayout без text/content-desc. // Алгоритм не зависит от разрешения: ищем строки из ровно 3 кликабельных узлов. @@ -1035,6 +1210,19 @@ QList > ScreenXmlParser::parseAccountsInfo() { return accounts; } +QString ScreenXmlParser::findFullCardNumber() { + static const QRegularExpression cardNumberRegex(R"(^\d{4}\s\d{4}\s\d{4}\s\d{4}$)"); + for (const Node &node : m_nodes) { + if (node.className == "android.widget.TextView") { + const QString text = node.text.trimmed(); + if (cardNumberRegex.match(text).hasMatch()) { + return QString(text).remove(QChar(' ')); + } + } + } + return {}; +} + void ScreenXmlParser::test() { // TransactionInfo info2 = parseExpandedInfo(m_xml); // qDebug().noquote().nospace() << convertTransactionToString(info2); diff --git a/android/ozon/ScreenXmlParser.h b/android/ozon/ScreenXmlParser.h index 5f520d7..b484567 100644 --- a/android/ozon/ScreenXmlParser.h +++ b/android/ozon/ScreenXmlParser.h @@ -34,6 +34,25 @@ public: bool isHomeScreen(); + Node tryToFindAdBanner(); + + // Returns a Button node whose text is exactly 4 digits (card last numbers) + Node findCardButtonOnHomeScreen(); + + // Finds a card button whose text ends with the given last 4 digits + Node findCardButtonByLastNumbers(const QString &lastNumbers); + + // Returns all cards visible on the home screen as AccountInfo stubs + // (lastNumbers, description, amount filled; id=-1, cardNumber empty) + QList parseCardsOnHomeScreen(); + + bool isProfileScreen(); + + Node findUserNameOnHomeScreen(int screenHeight); + + QString parseProfileFullName(); + + QString parseProfilePhone(); QList tryToFindPinCode(const QString &pinCode, const QString &title); @@ -51,6 +70,9 @@ public: QList > parseAccountsInfo(); + // Finds a TextView with full card number (format "XXXX XXXX XXXX XXXX") + QString findFullCardNumber(); + void test(); private: diff --git a/assets/ozon/card_screen.png b/assets/ozon/card_screen.png new file mode 100644 index 0000000..fffacb6 Binary files /dev/null and b/assets/ozon/card_screen.png differ diff --git a/assets/ozon/card_screen.xml b/assets/ozon/card_screen.xml new file mode 100644 index 0000000..a656a13 --- /dev/null +++ b/assets/ozon/card_screen.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/card_screen_hide.png b/assets/ozon/card_screen_hide.png new file mode 100644 index 0000000..8c23944 Binary files /dev/null and b/assets/ozon/card_screen_hide.png differ diff --git a/assets/ozon/card_screen_hide.xml b/assets/ozon/card_screen_hide.xml new file mode 100644 index 0000000..d40d81f --- /dev/null +++ b/assets/ozon/card_screen_hide.xml @@ -0,0 +1,821 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/card_screen_open.png b/assets/ozon/card_screen_open.png new file mode 100644 index 0000000..be5b92b Binary files /dev/null and b/assets/ozon/card_screen_open.png differ diff --git a/assets/ozon/card_screen_open.xml b/assets/ozon/card_screen_open.xml new file mode 100644 index 0000000..2355089 --- /dev/null +++ b/assets/ozon/card_screen_open.xml @@ -0,0 +1,872 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/home_page_2.png b/assets/ozon/home_page_2.png new file mode 100644 index 0000000..7cc4ae0 Binary files /dev/null and b/assets/ozon/home_page_2.png differ diff --git a/assets/ozon/home_page_2.xml b/assets/ozon/home_page_2.xml new file mode 100644 index 0000000..d553587 --- /dev/null +++ b/assets/ozon/home_page_2.xml @@ -0,0 +1,1552 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/last_transactions.png b/assets/ozon/last_transactions.png new file mode 100644 index 0000000..d4b530f Binary files /dev/null and b/assets/ozon/last_transactions.png differ diff --git a/assets/ozon/last_transactions.xml b/assets/ozon/last_transactions.xml new file mode 100644 index 0000000..a96240b --- /dev/null +++ b/assets/ozon/last_transactions.xml @@ -0,0 +1,1105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/profile.png b/assets/ozon/profile.png new file mode 100644 index 0000000..ca4c371 Binary files /dev/null and b/assets/ozon/profile.png differ diff --git a/assets/ozon/profile.xml b/assets/ozon/profile.xml new file mode 100644 index 0000000..ec1afd6 --- /dev/null +++ b/assets/ozon/profile.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/config.ini b/config.ini index 3e0d34d..1865920 100644 --- a/config.ini +++ b/config.ini @@ -1,18 +1,14 @@ [apps] list=ozon -[birbank] -android_package_name=az.kapitalbank.mbanking -name=Birbank - [common] -access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxODU0MzI0fQ.xLalNxKuPK1QV3D94Je1OJITBV-VfIQJG6iSwPa_mMg +access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyMjQ4MzM1fQ.gDA57WCWCCwdrxa84McLJLcIyV4RYLRQ7s9YT3AcFd4 api_key=0eee77b62013253e02e1a69bc6a505bb37757fb7 currencies=USD, KRW, AZN, RUB -desktop_id=6554b2a2-2885-430e-aae2-0045f82f068e +desktop_id=b86eac8e-dc89-4a9e-a5e8-7f85db011391 profile_id=c4552156-d179-4a79-9e91-587038c95cf0 -refresh_token=d22175aa-96ec-405d-8c9f-f6cfce1a0913 -token_expires_at=1771854324 +refresh_token=2cdf2bd7-9d5f-463f-98f4-fce79a9ee9a4 +token_expires_at=1772248335 version=0.0.1 [db] @@ -24,22 +20,23 @@ apiBase=http://93.183.75.134:8005 appInfoUpdate=http://localhost:9999/api/v1/device/update appStatusUpdate=http://localhost:9999/api/v1/application/update eventStatusUpdate=http://localhost:9999/api/v1/event/update +pathAuthLogin=/api/v1/auth/login +pathAuthRefresh=/api/v1/auth/refresh_token +pathBankProfile=/api/v1/profile/bank_profile +pathDesktopCreate=/api/v1/desktop/ +pathDevice=/api/v1/device/ +pathDeviceScreenshot=/api/v1/device/add_screenshot/ +pathMaterial=/api/v1/profile/material paymentsGet=http://localhost:9999/api/v1/payments transactionInsert=http://localhost:9999/api/v1/transaction/insert transactionUpdate=http://localhost:9999/api/v1/transaction/update [ozon] android_package_name=ru.ozon.fintech.finance +currency=RUB name=Ozon [rshb] android_package_name=ru.rshb.dbo +currency=RUB name=Росcельхозбанк - -[toss] -android_package_name=viva.republica.toss -name=Toss - -[ziraat] -android_package_name=com.ziraat.ziraatmobil -name=Ziraat diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index 2d7d138..19990cc 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -1,6 +1,7 @@ #include "DatabaseManager.h" #include #include +#include #include #include #include @@ -65,6 +66,213 @@ QString DatabaseManager::connectionName() { } +static bool columnExists(QSqlQuery &q, const QString &table, const QString &column) { + q.exec(QString("SELECT COUNT(*) FROM pragma_table_info('%1') WHERE name='%2'") + .arg(table, column)); + return q.next() && q.value(0).toInt() > 0; +} + +void DatabaseManager::initSchema() { + QSqlDatabase db = database(); + QSqlQuery q(db); + + // Читаем текущую версию схемы + q.exec("PRAGMA user_version"); + const int version = q.next() ? q.value(0).toInt() : 0; + + // ── Версия 1: начальная схема + api_id для devices ──────────────────────── + if (version < 1) { + db.transaction(); + + q.exec(R"(CREATE TABLE IF NOT EXISTS devices ( + id TEXT PRIMARY KEY, + api_id TEXT, + status TEXT, + name TEXT, + android TEXT, + screen_width INTEGER, + screen_height INTEGER, + battery INTEGER, + update_time DATETIME, + timestamp DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), + image BLOB + ))"); + + q.exec(R"(CREATE TABLE IF NOT EXISTS applications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT, + pin_code TEXT, + pin_code_checked TEXT DEFAULT 'no', + name TEXT, + code TEXT, + package TEXT, + status TEXT DEFAULT 'off', + install TEXT DEFAULT 'no', + comment TEXT, + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, + UNIQUE (device_id, code) + ))"); + + q.exec(R"(CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT, + app_code TEXT, + last_numbers TEXT, + description TEXT, + amount REAL, + currency TEXT DEFAULT '', + update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), + status TEXT DEFAULT 'active', + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, + UNIQUE (device_id, app_code, last_numbers) + ))"); + + q.exec(R"(CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER, + external_id INTEGER, + amount REAL, + fee REAL, + status TEXT, + type TEXT, + phone TEXT, + name TEXT, + short_description TEXT, + updated_short_description TEXT, + description TEXT, + updated_description TEXT, + bank_name TEXT, + bank_time TEXT, + bank_tr_external_id TEXT, + update_time DATETIME, + complete_time DATETIME, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + 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"; + } +} + void DatabaseManager::closeConnection() { QMutexLocker locker(&mutex); if (const QString name = connectionName(); connectionPool.contains(name)) { diff --git a/database/DatabaseManager.h b/database/DatabaseManager.h index b95ea61..8094b8a 100644 --- a/database/DatabaseManager.h +++ b/database/DatabaseManager.h @@ -11,6 +11,7 @@ public: static DatabaseManager& instance(); QSqlDatabase database(); + void initSchema(); void closeConnection(); private: diff --git a/database/app_data.db b/database/app_data.db index 20518d8..63fe766 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 611afb4..d8e736b 100644 --- a/database/dao/AccountDAO.cpp +++ b/database/dao/AccountDAO.cpp @@ -10,9 +10,9 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - INSERT INTO accounts (device_id, app_name, last_numbers, amount, update_time, status, description, currency) - VALUES (:device_id, :app_name, :last_numbers, :amount, :update_time, :status, :description, :currency) - ON CONFLICT(device_id, app_name, last_numbers) + INSERT INTO accounts (device_id, app_code, last_numbers, amount, update_time, status, description, currency) + VALUES (:device_id, :app_code, :last_numbers, :amount, :update_time, :status, :description, :currency) + ON CONFLICT(device_id, app_code, last_numbers) DO UPDATE SET amount = excluded.amount, update_time = excluded.update_time, @@ -23,7 +23,7 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) { )"); query.bindValue(":device_id", info.deviceId); - query.bindValue(":app_name", info.appCode); + query.bindValue(":app_code", info.appCode); query.bindValue(":last_numbers", info.lastNumbers); query.bindValue(":currency", info.currency); query.bindValue(":description", info.description); @@ -77,12 +77,40 @@ bool AccountDAO::deleteAccount(const int id) { return query.exec(); } +bool AccountDAO::updateCardNumber(const int accountId, const QString &cardNumber) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE accounts SET card_number = :card_number WHERE id = :id + )"); + query.bindValue(":card_number", cardNumber); + query.bindValue(":id", accountId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении card_number:" << query.lastError().text(); + return false; + } + return true; +} + +bool AccountDAO::updateMaterialId(const int accountId, const QString &materialId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE accounts SET material_id = :material_id WHERE id = :id"); + query.bindValue(":material_id", materialId); + query.bindValue(":id", accountId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении material_id:" << query.lastError().text(); + return false; + } + return true; +} + AccountInfo parseAccount(const QSqlQuery &query) { AccountInfo acc; acc.id = query.value("id").toInt(); acc.deviceId = query.value("device_id").toString(); - acc.appCode = query.value("app_name").toString(); + acc.appCode = query.value("app_code").toString(); acc.lastNumbers = query.value("last_numbers").toString(); + acc.cardNumber = query.value("card_number").toString(); + acc.materialId = query.value("material_id").toString(); acc.amount = query.value("amount").toDouble(); acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString()); acc.status = accountStatusFromString(query.value("status").toString()); @@ -96,13 +124,13 @@ QList AccountDAO::getAccounts(const QString &deviceId, const QStrin QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_name, last_numbers, amount, update_time, status, description, currency + SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency FROM accounts - WHERE device_id = :device_id AND app_name = :app_name + WHERE device_id = :device_id AND app_code = :app_code )"); query.bindValue(":device_id", deviceId); - query.bindValue(":app_name", appName); + query.bindValue(":app_code", appName); if (!query.exec()) { qWarning() << "Ошибка при получении accounts:" << query.lastError().text(); @@ -120,7 +148,7 @@ AccountInfo AccountDAO::getAccountById(const int accountId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_name, last_numbers, amount, update_time, status, description, currency + SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency FROM accounts WHERE id = :id LIMIT 1 @@ -143,9 +171,9 @@ AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &a QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_name, last_numbers, amount, update_time, status, description, currency + SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency FROM accounts - WHERE app_name = :appName AND last_numbers = :lastNumber AND device_id = :deviceId + WHERE app_code = :appName AND last_numbers = :lastNumber AND device_id = :deviceId LIMIT 1 )"); diff --git a/database/dao/AccountDAO.h b/database/dao/AccountDAO.h index 28118d4..f869d47 100644 --- a/database/dao/AccountDAO.h +++ b/database/dao/AccountDAO.h @@ -14,6 +14,10 @@ public: [[nodiscard]] static bool deleteAccount(int id); + [[nodiscard]] static bool updateCardNumber(int accountId, const QString &cardNumber); + + [[nodiscard]] static bool updateMaterialId(int accountId, const QString &materialId); + [[nodiscard]] static QList getAccounts(const QString &deviceId, const QString &appName); [[nodiscard]] static AccountInfo getAccountById(int accountId); diff --git a/database/dao/ApplicationDAO.cpp b/database/dao/ApplicationDAO.cpp index c339ebb..78f688b 100644 --- a/database/dao/ApplicationDAO.cpp +++ b/database/dao/ApplicationDAO.cpp @@ -22,7 +22,7 @@ bool ApplicationDAO::upsertApplication(const ApplicationInfo &info) { status = excluded.status, comment = excluded.comment, install = excluded.install, - pin_code_checked = excluded.pin_code_checked, + pin_code_checked = excluded.pin_code_checked )"); query.bindValue(":id", info.id); @@ -127,6 +127,11 @@ ApplicationInfo parseApplication(const QSqlQuery &query) { app.comment = query.value("comment").toString(); app.install = query.value("install").toString() == "yes"; app.pinCodeChecked = query.value("pin_code_checked").toString() == "yes"; + app.phone = query.value("phone").toString(); + app.fullName = query.value("full_name").toString(); + app.bankProfileId = query.value("bank_profile_id").toString(); + app.currency = query.value("currency").toString(); + app.amount = query.value("amount").toDouble(); return app; } @@ -134,7 +139,7 @@ ApplicationInfo ApplicationDAO::getApplication(const QString &deviceId, const QS QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, full_name, bank_profile_id, currency, amount FROM applications WHERE device_id = :device_id AND code = :code LIMIT 1 @@ -159,7 +164,7 @@ QList ApplicationDAO::getApplicationsByDeviceId(const QString & QList list; query.prepare(R"( - SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, full_name, bank_profile_id, currency, amount FROM applications WHERE device_id = :device_id AND install = 'yes' )"); @@ -177,6 +182,82 @@ QList ApplicationDAO::getApplicationsByDeviceId(const QString & return list; } +bool ApplicationDAO::updateProfileInfo( + const int &appId, + const QString &fullName, + const QString &phone +) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + UPDATE applications + SET full_name = :full_name, phone = :phone + WHERE id = :id + )"); + + query.bindValue(":id", appId); + query.bindValue(":full_name", fullName); + query.bindValue(":phone", phone); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении профиля приложения:" << query.lastError().text(); + return false; + } + return true; +} + +bool ApplicationDAO::updateAmount(const int appId, const double amount) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE applications SET amount = :amount WHERE id = :id"); + query.bindValue(":amount", amount); + query.bindValue(":id", appId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении amount в application:" << query.lastError().text(); + return false; + } + return true; +} + +bool ApplicationDAO::updateBankProfileId(const int appId, const QString &bankProfileId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE applications SET bank_profile_id = :bank_profile_id WHERE id = :id"); + query.bindValue(":bank_profile_id", bankProfileId); + query.bindValue(":id", appId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении bank_profile_id:" << query.lastError().text(); + return false; + } + return true; +} + +bool ApplicationDAO::activateAppsForDevice(const QString &deviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE applications SET status = 'active' + WHERE device_id = :device_id AND install = 'yes' AND pin_code_checked = 'yes' + )"); + query.bindValue(":device_id", deviceId); + if (!query.exec()) { + qWarning() << "Ошибка при активации приложений:" << query.lastError().text(); + return false; + } + return true; +} + +bool ApplicationDAO::deactivateAppsForDevice(const QString &deviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE applications SET status = 'off' + WHERE device_id = :device_id AND install = 'yes' + )"); + query.bindValue(":device_id", deviceId); + if (!query.exec()) { + qWarning() << "Ошибка при деактивации приложений:" << query.lastError().text(); + return false; + } + return true; +} + bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet &apps) { QSqlQuery query(DatabaseManager::instance().database()); const QSettings settings("config.ini", QSettings::IniFormat); @@ -201,12 +282,15 @@ bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet getApplicationsByDeviceId(const QString &deviceId); [[nodiscard]] static bool checkInstalledApps(const QString &deviceId, const QSet &apps); + + [[nodiscard]] static bool activateAppsForDevice(const QString &deviceId); + + [[nodiscard]] static bool deactivateAppsForDevice(const QString &deviceId); }; diff --git a/database/dao/DeviceDAO.cpp b/database/dao/DeviceDAO.cpp index 354ddbc..3150154 100644 --- a/database/dao/DeviceDAO.cpp +++ b/database/dao/DeviceDAO.cpp @@ -20,12 +20,6 @@ DeviceInfo parseDevice(const QSqlQuery &query) { return device; } -void DeviceDAO::migrate() { - QSqlQuery query(DatabaseManager::instance().database()); - // Добавляем колонку если её ещё нет (SQLite вернёт ошибку — игнорируем) - query.exec("ALTER TABLE devices ADD COLUMN api_id TEXT"); -} - QList DeviceDAO::getAllDevices(const bool online) { QList devices; @@ -33,10 +27,9 @@ QList DeviceDAO::getAllDevices(const bool online) { QString sql = R"( SELECT id, api_id, status, name, android, screen_width, screen_height, battery, update_time, image FROM devices - WHERE image IS NOT NULL )"; if (online) { - sql += " AND status = 'device'"; + sql += " WHERE status = 'device'"; } query.prepare(sql); diff --git a/database/dao/DeviceDAO.h b/database/dao/DeviceDAO.h index 069d8ab..ca68093 100644 --- a/database/dao/DeviceDAO.h +++ b/database/dao/DeviceDAO.h @@ -4,8 +4,6 @@ class DeviceDAO { public: - static void migrate(); - [[nodiscard]] static DeviceInfo getDeviceById(const QString &deviceId); [[nodiscard]] static bool upsertDevice(const DeviceInfo &device); diff --git a/main.cpp b/main.cpp index 5051fa5..16fade6 100644 --- a/main.cpp +++ b/main.cpp @@ -156,8 +156,8 @@ int main(int argc, char *argv[]) { qInstallMessageHandler(messageHandler); - // Миграции БД (добавляем новые колонки если их ещё нет) - DeviceDAO::migrate(); + // Миграции БД + DatabaseManager::instance().initSchema(); // 1. Если есть api_key — логинимся и проверяем/создаём desktop // - ошибка сети или сервера [code -1] @@ -191,9 +191,35 @@ int main(int argc, char *argv[]) { QObject::connect(loader, &LoaderWindow::loadingFinished, loader, [loader, &app, showRegistration](const int result) { if (result == -1) { auto *errorWindow = new ErrorWindow( - "Нет подключения к интернету или сервер не отвечает.\n\nПожалуйста, попробуйте позже." + "Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.", + true ); errorWindow->show(); + QObject::connect(errorWindow, &ErrorWindow::retryRequested, errorWindow, [errorWindow, &app, showRegistration]() { + errorWindow->close(); + auto *newLoader = new LoaderWindow(); + newLoader->show(); + QObject::connect(newLoader, &LoaderWindow::loadingFinished, newLoader, [newLoader, &app, showRegistration](const int r) { + if (r == 1) { + auto *mainWindow = new MainWindow(); + mainWindow->show(); + startNetworkService(app); + startDeviceScreener(app); + startEventHandler(app); + } else if (r == 0) { + QSettings s("config.ini", QSettings::IniFormat); + s.remove("common/api_key"); + showRegistration(); + } else { + auto *err2 = new ErrorWindow( + "Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.", + false + ); + err2->show(); + } + newLoader->close(); + }, Qt::QueuedConnection); + }); } else if (result == 1) { auto *mainWindow = new MainWindow(); mainWindow->show(); diff --git a/migrations.sql b/migrations.sql index 7c9ef3c..ab0cc29 100644 --- a/migrations.sql +++ b/migrations.sql @@ -1,3 +1,7 @@ +-- ============================================================ +-- Версия 1 (user_version = 1): начальная схема +-- ============================================================ + CREATE TABLE IF NOT EXISTS devices ( id TEXT PRIMARY KEY, @@ -18,29 +22,31 @@ CREATE TABLE IF NOT EXISTS applications id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT, pin_code TEXT, - pin_code_checked TEXT, + pin_code_checked TEXT DEFAULT 'no', name TEXT, code TEXT, package TEXT, - status TEXT, - install TEXT, + status TEXT DEFAULT 'off', + install TEXT DEFAULT 'no', comment TEXT, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, UNIQUE (device_id, code) ); +-- app_code: код банка ("ozon", "birbank", ...) — не путать с человекочитаемым name CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT, - app_name TEXT, + app_code TEXT, last_numbers TEXT, description TEXT, amount REAL, + currency TEXT DEFAULT '', update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), - status TEXT, + status TEXT DEFAULT 'active', FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, - UNIQUE (device_id, app_name, last_numbers) + UNIQUE (device_id, app_code, last_numbers) ); CREATE TABLE IF NOT EXISTS transactions @@ -48,23 +54,19 @@ CREATE TABLE IF NOT EXISTS transactions id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER, external_id INTEGER, - amount REAL, fee REAL, status TEXT, type TEXT, phone TEXT, name TEXT, - short_description TEXT, updated_short_description TEXT, description TEXT, updated_description TEXT, - bank_name TEXT, bank_time TEXT, bank_tr_external_id TEXT, - update_time DATETIME, complete_time DATETIME, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, @@ -77,19 +79,24 @@ CREATE TABLE IF NOT EXISTS events 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, + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, UNIQUE (external_id) -); \ No newline at end of file +); + +-- ============================================================ +-- Версия 2 (user_version = 2): currency + rename app_name→app_code +-- ============================================================ + +-- Для существующих БД (применяется автоматически при старте): +-- ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT ''; +-- ALTER TABLE accounts RENAME COLUMN app_name TO app_code; diff --git a/models/db/AccountInfo.h b/models/db/AccountInfo.h index 2adb1c2..216a5e3 100644 --- a/models/db/AccountInfo.h +++ b/models/db/AccountInfo.h @@ -34,6 +34,8 @@ struct AccountInfo { QString deviceId; // code QString appCode; QString lastNumbers; + QString cardNumber; // full 16-digit card number + QString materialId; QString currency; QString description; double amount = 0.0; diff --git a/models/db/ApplicationInfo.h b/models/db/ApplicationInfo.h index e7cf8ca..116f754 100644 --- a/models/db/ApplicationInfo.h +++ b/models/db/ApplicationInfo.h @@ -15,5 +15,10 @@ struct ApplicationInfo { QString package; QString status; QString comment; + QString phone; + QString fullName; + QString bankProfileId; + QString currency; + double amount = 0.0; QDateTime updateTime; }; diff --git a/services/DeviceScreener.cpp b/services/DeviceScreener.cpp index f54d68d..d349282 100644 --- a/services/DeviceScreener.cpp +++ b/services/DeviceScreener.cpp @@ -174,6 +174,43 @@ QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) { return {}; } +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()); + + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpPart filePart; + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, + QStringLiteral("form-data; name=\"file\"; filename=\"screenshot.jpg\"")); + filePart.setBody(image); + multiPart->append(filePart); + + QNetworkReply *reply = m_manager->post(request, multiPart); + multiPart->setParent(reply); + QEventLoop loop; + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + loop.exec(); + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "postScreenshotToApi network error:" << reply->errorString(); + } else { + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + if (doc.isObject() && doc.object()["success"].toBool()) { + qDebug() << "postScreenshotToApi done for device" << apiId; + } else { + qWarning() << "postScreenshotToApi failed for device" << apiId << ":" << doc.toJson(QJsonDocument::Compact); + } + } + reply->deleteLater(); +} + void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) { QJsonObject body; body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE"; @@ -197,16 +234,23 @@ void DeviceScreener::start() { const bool isChecked = bankChecked.contains(id); device = readDeviceInfo(id, isChecked); device.image = takeScreenshot(id); - if (!DeviceDAO::updateImage(id, device.image)) { - qDebug() << "Cant update screenshot"; + if (!device.image.isEmpty()) { + if (!DeviceDAO::updateImage(id, device.image)) { + qDebug() << "Cant update screenshot"; + } + } + if (!isChecked) { + ApplicationDAO::activateAppsForDevice(id); } bankChecked[id] = true; } device.status = status; device.updateTime = QDateTime::currentDateTime(); deviceInfos.append(device); - if (!DeviceDAO::upsertDevice(device)) { - qDebug() << "Cant update devive"; + if (status == "device") { + if (!DeviceDAO::upsertDevice(device)) { + qDebug() << "Cant update device"; + } } // Синхронизация с API @@ -224,6 +268,9 @@ void DeviceScreener::start() { if (now - m_lastApiUpdate.value(device.id, 0) >= 60) { device.apiId = saved.apiId; updateDeviceOnApi(device); + if (!device.image.isEmpty()) { + postScreenshotToApi(saved.apiId, device.image); + } m_lastApiUpdate[device.id] = now; } } @@ -236,6 +283,18 @@ void DeviceScreener::start() { if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) { qDebug() << "Cant mark devices offline"; } + + // Деактивировать приложения для устройств, которые ушли в офлайн + QStringList offlineIds; + for (auto it = bankChecked.begin(); it != bankChecked.end();) { + if (!onlineIds.contains(it.key())) { + ApplicationDAO::deactivateAppsForDevice(it.key()); + it = bankChecked.erase(it); + } else { + ++it; + } + } + QThread::msleep(1000); } emit finished(); diff --git a/services/DeviceScreener.h b/services/DeviceScreener.h index d42f84c..f1c760e 100644 --- a/services/DeviceScreener.h +++ b/services/DeviceScreener.h @@ -41,4 +41,5 @@ private: QString createDeviceOnApi(const DeviceInfo &device); void updateDeviceOnApi(const DeviceInfo &device); + void postScreenshotToApi(const QString &apiId, const QByteArray &image); }; diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index 55772c0..2387da8 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -11,6 +11,7 @@ #include #include "dao/AccountDAO.h" +#include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" #include "dao/EventDAO.h" #include "dao/TransactionDAO.h" @@ -25,6 +26,13 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) { m_urlTransactionInsert = settings.value("net/transactionInsert").toString(); m_urlEventStatusUpdate = settings.value("net/eventStatusUpdate").toString(); m_apiBase = settings.value("net/apiBase").toString(); + m_pathAuthLogin = settings.value("net/pathAuthLogin", "/api/v1/auth/login").toString(); + m_pathAuthRefresh = settings.value("net/pathAuthRefresh", "/api/v1/auth/refresh_token").toString(); + m_pathDesktopCreate = settings.value("net/pathDesktopCreate", "/api/v1/desktop/").toString(); + m_pathDevice = settings.value("net/pathDevice", "/api/v1/device/").toString(); + m_pathDeviceScreenshot= settings.value("net/pathDeviceScreenshot","/api/v1/device/add_screenshot/").toString(); + m_pathBankProfile = settings.value("net/pathBankProfile", "/api/v1/profile/bank_profile").toString(); + m_pathMaterial = settings.value("net/pathMaterial", "/api/v1/profile/material").toString(); m_manager = new QNetworkAccessManager(this); m_token = settings.value("common/token").toString(); m_accessToken = settings.value("common/access_token").toString(); @@ -199,7 +207,9 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body QJsonValue result; if (reply->error() != QNetworkReply::NoError) { - qCritical() << "Network error:" << reply->errorString() << path; + const QByteArray responseBody = reply->readAll(); + qCritical() << "Network error (POST):" << reply->errorString() << path + << "\n response body:" << responseBody; result = QJsonValue::Undefined; } else { const QByteArray raw = reply->readAll(); @@ -226,7 +236,7 @@ void NetworkService::loginAndSetup() { // Шаг 1: логин QJsonObject loginBody; loginBody["api_key"] = apiKey; - const QJsonValue loginResult = postJson(m_apiBase + "/api/v1/auth/login", loginBody); + const QJsonValue loginResult = postJson(m_apiBase + m_pathAuthLogin, loginBody); if (loginResult.isUndefined()) { emit finishedWithResult(-1); // ошибка сети @@ -260,7 +270,7 @@ void NetworkService::loginAndSetup() { desktopBody["name"] = QJsonValue::Null; desktopBody["contact"] = QJsonValue::Null; desktopBody["app_version"] = settings.value("common/version").toString(); - const QJsonValue desktopResult = postJson(m_apiBase + "/api/v1/desktop/", desktopBody, true); + const QJsonValue desktopResult = postJson(m_apiBase + m_pathDesktopCreate, desktopBody, true); if (desktopResult.isUndefined() || desktopResult.isNull()) { emit finishedWithResult(-1); @@ -340,7 +350,9 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod QJsonValue result; if (reply->error() != QNetworkReply::NoError) { - qCritical() << "Network error (PATCH):" << reply->errorString() << path; + const QByteArray responseBody = reply->readAll(); + qCritical() << "Network error (PATCH):" << reply->errorString() << path + << "\n response body:" << responseBody; } else { const QByteArray raw = reply->readAll(); QJsonParseError err; @@ -358,6 +370,112 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod return result; } +void NetworkService::postBankProfile() { + if (!ensureValidToken()) { emit finished(); return; } + + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + qDebug() << "[NetworkService] postBankProfile payload:" << QJsonDocument(body).toJson(QJsonDocument::Compact); + const QJsonValue result = postJson(m_apiBase + m_pathBankProfile, body, true); + if (result.isNull() || result.isUndefined()) { + qWarning() << "[NetworkService] postBankProfile failed"; + } else { + const QJsonObject data = result.toObject(); + const QString bankProfileId = data["id"].toString(); + const int appId = m_extraData.value("app_id").toInt(); + if (!bankProfileId.isEmpty() && appId > 0) { + ApplicationDAO::updateBankProfileId(appId, bankProfileId); + qDebug() << "[NetworkService] postBankProfile saved bank_profile_id:" << bankProfileId; + } + } + + emit finished(); +} + +void NetworkService::patchBankProfile() { + if (!ensureValidToken()) { emit finished(); return; } + + const QString bankProfileId = m_extraData.value("bank_profile_id").toString(); + if (bankProfileId.isEmpty()) { + qWarning() << "[NetworkService] patchBankProfile: bank_profile_id is empty"; + emit finished(); + return; + } + + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + patchJson(m_apiBase + m_pathBankProfile + "/" + bankProfileId, body, true); + qDebug() << "[NetworkService] patchBankProfile done for id:" << bankProfileId; + + emit finished(); +} + +void NetworkService::postMaterial() { + if (!ensureValidToken()) { emit finished(); return; } + + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + qDebug() << "[NetworkService] postMaterial body:" << QJsonDocument(body).toJson(QJsonDocument::Compact); + const QJsonValue result = postJson(m_apiBase + m_pathMaterial, body, true); + if (result.isNull() || result.isUndefined()) { + qWarning() << "[NetworkService] postMaterial failed, card:" << body["card_number"].toString(); + } else { + const QString materialId = result.toObject()["id"].toString(); + const int accountId = m_extraData.value("account_id").toInt(); + if (!materialId.isEmpty() && accountId > 0) { + AccountDAO::updateMaterialId(accountId, materialId); + } + qDebug() << "[NetworkService] postMaterial ok, card:" << body["card_number"].toString() << "material_id:" << materialId; + } + + emit finished(); +} + +void NetworkService::postDeviceScreenshot() { + if (!ensureValidToken()) { emit finished(); return; } + + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + if (device.apiId.isEmpty()) { + qWarning() << "[NetworkService] postDeviceScreenshot: no api_id for" << m_deviceId; + emit finished(); + return; + } + if (m_json.isEmpty()) { + qWarning() << "[NetworkService] postDeviceScreenshot: no image data"; + emit finished(); + return; + } + + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpPart filePart; + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg"); + filePart.setHeader( + QNetworkRequest::ContentDispositionHeader, + QStringLiteral("form-data; name=\"file\"; filename=\"%1.jpg\"").arg(m_deviceId) + ); + filePart.setBody(m_json); + multiPart->append(filePart); + + const QUrl url(m_apiBase + m_pathDeviceScreenshot + device.apiId); + QNetworkRequest request(url); + request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + + QNetworkReply *reply = m_manager->post(request, multiPart); + multiPart->setParent(reply); + + QEventLoop loop; + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { + loop.quit(); + }); + loop.exec(); + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[NetworkService] postDeviceScreenshot failed:" << reply->errorString(); + } else { + qDebug() << "[NetworkService] postDeviceScreenshot ok for device" << device.apiId; + } + reply->deleteLater(); + emit finished(); +} + void NetworkService::createDevice() { const QSettings settings("config.ini", QSettings::IniFormat); const QString desktopId = settings.value("common/desktop_id").toString(); @@ -368,7 +486,7 @@ void NetworkService::createDevice() { } const QJsonObject body = QJsonDocument::fromJson(m_json).object(); - const QJsonValue result = postJson(m_apiBase + "/api/v1/device/", body, true); + const QJsonValue result = postJson(m_apiBase + m_pathDevice, body, true); if (!result.isNull() && !result.isUndefined()) { const QString apiId = result.toObject()["id"].toString(); @@ -390,7 +508,7 @@ void NetworkService::updateDevice() { } const QJsonObject body = QJsonDocument::fromJson(m_json).object(); - patchJson(m_apiBase + "/api/v1/device/" + device.apiId, body, true); + patchJson(m_apiBase + m_pathDevice + device.apiId, body, true); emit finished(); } @@ -405,7 +523,7 @@ bool NetworkService::refreshToken() { QJsonObject body; body["refresh_token"] = storedRefreshToken; - const QJsonValue result = postJson(m_apiBase + "/api/v1/auth/refresh_token", body); + const QJsonValue result = postJson(m_apiBase + m_pathAuthRefresh, body); if (result.isNull() || result.isUndefined()) { qCritical() << "refreshToken: не удалось обновить токен"; diff --git a/services/net/NetworkService.h b/services/net/NetworkService.h index 16abe6a..60312a6 100644 --- a/services/net/NetworkService.h +++ b/services/net/NetworkService.h @@ -23,6 +23,14 @@ public: public slots: void postAccountsData(); + void postBankProfile(); + + void patchBankProfile(); + + void postMaterial(); + + void postDeviceScreenshot(); + void getPayments(); void postAppStatus(); @@ -63,6 +71,13 @@ private: QString m_urlAppStatusUpdate; QString m_urlTransactionInsert; QString m_urlEventStatusUpdate; + QString m_pathAuthLogin; + QString m_pathAuthRefresh; + QString m_pathDesktopCreate; + QString m_pathDevice; + QString m_pathDeviceScreenshot; + QString m_pathBankProfile; + QString m_pathMaterial; QNetworkAccessManager *m_manager; diff --git a/views/MainWindow.cpp b/views/MainWindow.cpp index 996b8e3..a06415e 100644 --- a/views/MainWindow.cpp +++ b/views/MainWindow.cpp @@ -81,7 +81,7 @@ void MainWindow::createDevicePage() { // flowLayout->setContentsMargins(0, 0, 0, 0); // flowLayout->setSpacing(0); - QVector devices = DeviceDAO::getAllDevices(true); + QVector devices = DeviceDAO::getAllDevices(false); for (const auto &device: devices) { auto *deviceWidget = new DeviceWidget(device); // deviceWidget->setFixedSize(220, 400); @@ -116,7 +116,7 @@ void MainWindow::deleteDevicePage() { * Для того, чтобы не нарушать константность, этот метод должен быть неконстантным (без const в сигнатуре). */ void MainWindow::loadDevices() { - const auto devices = DeviceDAO::getAllDevices(true); + const auto devices = DeviceDAO::getAllDevices(false); // Очистить старые виджеты QLayoutItem *item; while ((item = flowLayout->takeAt(0)) != nullptr) { diff --git a/views/bank/AccountSettingsWindow.cpp b/views/bank/AccountSettingsWindow.cpp index 802f582..bf6c9ea 100644 --- a/views/bank/AccountSettingsWindow.cpp +++ b/views/bank/AccountSettingsWindow.cpp @@ -13,6 +13,8 @@ #include "birbank/LoginAndCheckAccountsBirbankScript.h" #include "ozon/LoginAndCheckAccountsScript.h" +#include "ozon/GetProfileInfoScript.h" +#include "ozon/GetCardInfoScript.h" #include "toss/LoginAndCheckAccountsTossScript.h" #include "dao/AccountDAO.h" #include "dao/ApplicationDAO.h" @@ -115,6 +117,19 @@ AccountSettingsWindow::AccountSettingsWindow( commentRow->addWidget(m_commentLabel); layout->addLayout(commentRow); + QHBoxLayout *profileRow = new QHBoxLayout(); + QString profileText; + if (m_applicationInfo.fullName.isEmpty() && m_applicationInfo.phone.isEmpty()) { + profileText = "Данные не получены"; + } else { + profileText = m_applicationInfo.fullName + " | " + m_applicationInfo.phone; + } + m_profileLabel = new QLabel(profileText); + m_profileLabel->setTextFormat(Qt::RichText); + m_profileLabel->setStyleSheet("margin-bottom: 8px;"); + profileRow->addWidget(m_profileLabel); + layout->addLayout(profileRow); + QPushButton *soloButton = new QPushButton("Запустить проверку пин-кода"); soloButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); // soloButton->setStyleSheet(""); @@ -123,27 +138,78 @@ AccountSettingsWindow::AccountSettingsWindow( "color: white;" ); QObject::connect(soloButton, &QPushButton::clicked, [&, this]() { - if (!m_applicationInfo.pinCode.isEmpty()) { - QMessageBox msgBox(this); - msgBox.setWindowTitle(tr("Запустить проверку пин-кода?")); - msgBox.setText( - "Запуститься скрипт проверки входа в банк " + m_applicationInfo.name + - " по пин-коду. Окно с настройками закроется."); + if (m_applicationInfo.pinCode.isEmpty()) { + QMessageBox::warning(this, "Пин-код не задан", + "Сначала укажите пин-код банка через кнопку \"Редактировать\"."); + return; + } + QMessageBox msgBox(this); + msgBox.setWindowTitle(tr("Запустить проверку пин-кода?")); + msgBox.setText( + "Запуститься скрипт проверки входа в банк " + m_applicationInfo.name + + " по пин-коду. Окно с настройками закроется."); - QPushButton *deleteBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole); - QPushButton *closeBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole); - msgBox.setDefaultButton(closeBtn); - msgBox.setModal(true); - msgBox.exec(); + QPushButton *deleteBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole); + QPushButton *closeBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole); + msgBox.setDefaultButton(closeBtn); + msgBox.setModal(true); + msgBox.exec(); - if (msgBox.clickedButton() == deleteBtn) { - startCheckPinCode(m_applicationInfo.code); - close(); - } + if (msgBox.clickedButton() == deleteBtn) { + startCheckPinCode(m_applicationInfo.code); + close(); } }); layout->addWidget(soloButton); + if (m_applicationInfo.code == "ozon" && m_applicationInfo.pinCodeChecked) { + QHBoxLayout *ozonRow = new QHBoxLayout(); + + QPushButton *profileButton = new QPushButton("Обновить профиль"); + profileButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + profileButton->setStyleSheet("background-color: #1565C0; color: white;"); + QObject::connect(profileButton, &QPushButton::clicked, [this]() { + QMessageBox msgBox(this); + msgBox.setWindowTitle(tr("Обновить профиль?")); + msgBox.setText( + "Запустится скрипт получения профиля из " + m_applicationInfo.name + + ". Окно с настройками закроется."); + QPushButton *runBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole); + QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole); + msgBox.setDefaultButton(cancelBtn); + msgBox.setModal(true); + msgBox.exec(); + if (msgBox.clickedButton() == runBtn) { + startGetProfileInfo(m_applicationInfo.code); + close(); + } + }); + + QPushButton *cardButton = new QPushButton("Проверить аккаунты"); + cardButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + cardButton->setStyleSheet("background-color: #1565C0; color: white;"); + QObject::connect(cardButton, &QPushButton::clicked, [this]() { + QMessageBox msgBox(this); + msgBox.setWindowTitle(tr("Проверить аккаунты?")); + msgBox.setText( + "Запустится скрипт получения данных карты из " + m_applicationInfo.name + + ". Окно с настройками закроется."); + QPushButton *runBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole); + QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole); + msgBox.setDefaultButton(cancelBtn); + msgBox.setModal(true); + msgBox.exec(); + if (msgBox.clickedButton() == runBtn) { + startGetCardInfo(m_applicationInfo.code); + close(); + } + }); + + ozonRow->addWidget(profileButton); + ozonRow->addWidget(cardButton); + ozonRow->addStretch(); + layout->addLayout(ozonRow); + } layout->addWidget(new QLabel("Счета")); QTableWidget *tableWidget = new QTableWidget(this); @@ -230,6 +296,32 @@ void AccountSettingsWindow::startCheckPinCode(const QString &appCode) { thread->start(); } +void AccountSettingsWindow::startGetProfileInfo(const QString &appCode) { + if (appCode != "ozon") return; + + QThread *thread = new QThread(nullptr); + auto *worker = new Ozon::GetProfileInfoScript(m_deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Ozon::GetProfileInfoScript::start); + connect(worker, &Ozon::GetProfileInfoScript::finished, thread, &QThread::quit); + connect(worker, &Ozon::GetProfileInfoScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + thread->start(); +} + +void AccountSettingsWindow::startGetCardInfo(const QString &appCode) { + if (appCode != "ozon") return; + + QThread *thread = new QThread(nullptr); + auto *worker = new Ozon::GetCardInfoScript(m_deviceId, appCode, "", nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Ozon::GetCardInfoScript::start); + connect(worker, &Ozon::GetCardInfoScript::finished, thread, &QThread::quit); + connect(worker, &Ozon::GetCardInfoScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + thread->start(); +} + void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) { Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status) // TODO: заменить на новый API @@ -291,7 +383,8 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) { time->setFlags(time->flags() & ~Qt::ItemIsEditable); tableWidget->setItem(k, 0, time); - QTableWidgetItem *desc = new QTableWidgetItem(account.description); + const QString displayNumber = account.cardNumber.isEmpty() ? account.description : account.cardNumber; + QTableWidgetItem *desc = new QTableWidgetItem(displayNumber); desc->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); desc->setFlags(desc->flags() & ~Qt::ItemIsEditable); tableWidget->setItem(k, 1, desc); diff --git a/views/bank/AccountSettingsWindow.h b/views/bank/AccountSettingsWindow.h index 91b7721..b1c2810 100644 --- a/views/bank/AccountSettingsWindow.h +++ b/views/bank/AccountSettingsWindow.h @@ -19,9 +19,14 @@ private: QLabel *m_statusLabel; QLabel *m_commentLabel; + QLabel *m_profileLabel; void startCheckPinCode(const QString &appCode); + void startGetProfileInfo(const QString &appCode); + + void startGetCardInfo(const QString &appCode); + void reloadContent(QTableWidget *tableWidget); void sendAppStatus(const QString &deviceId, const QString &code, const QString &status); diff --git a/views/device/DeviceWidget.cpp b/views/device/DeviceWidget.cpp index 6149370..b99d51f 100644 --- a/views/device/DeviceWidget.cpp +++ b/views/device/DeviceWidget.cpp @@ -37,7 +37,7 @@ void sendBankStatus(const QString &deviceId, const QString &code, const QString // TODO: заменить на новый API } -void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent) { +void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline) { rowWidget->setContentsMargins(0, 0, 0, 0); rowWidget->setStyleSheet( "background-color: rgba(0, 0, 0, 0.6);" // полупрозрачный фон @@ -90,6 +90,9 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, "border: none;" ); + if (isOffline) { + btn1->setEnabled(false); + } connect(btn1, &QPushButton::clicked, this, [app]() { QString newStatus = app.status == "active" ? "off" : "active"; QMessageBox msgBox(nullptr); @@ -182,11 +185,15 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p layout->setSpacing(0); this->setStyleSheet("background-color: lightgray;padding: 2px;"); - QImage img; - img.loadFromData(device.image, "JPG"); - auto *imageLabel = new QLabel(this); - imageLabel->setPixmap(QPixmap::fromImage(img).scaled(300, 450, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + if (!device.image.isEmpty()) { + QImage img; + img.loadFromData(device.image, "JPG"); + imageLabel->setPixmap(QPixmap::fromImage(img).scaled(300, 450, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } else { + imageLabel->setFixedSize(300, 450); + imageLabel->setStyleSheet("background-color: #555;"); + } imageLabel->setAlignment(Qt::AlignCenter); layout->addWidget(imageLabel, 0, 0); @@ -210,6 +217,20 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p // статус — в ту же ячейку, но поверх картинки и по углу layout->addWidget(batteryLabel, 0, 0, Qt::AlignTop | Qt::AlignRight); + if (device.status != "device") { + auto *offlineLabel = new QLabel("Офлайн", this); + offlineLabel->setAlignment(Qt::AlignCenter); + offlineLabel->setStyleSheet( + "background-color: rgba(0, 0, 0, 0.6);" + "color: white;" + "font-size: 24px;" + "font-weight: bold;" + "padding: 10px 20px;" + "border-radius: 8px;" + ); + layout->addWidget(offlineLabel, 0, 0, Qt::AlignCenter); + } + auto *appsView = new QWidget(this); appsView->setAttribute(Qt::WA_TransparentForMouseEvents, false); @@ -219,11 +240,13 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p appsBoxLayout->setContentsMargins(2, 0, 0, 2); appsBoxLayout->setSpacing(2); + const bool isOffline = device.status != "device"; + QList apps = ApplicationDAO::getApplicationsByDeviceId(device.id); for (ApplicationInfo &app: apps) { if (app.install) { auto *rowWidget = new QWidget(appsView); - buildBankRow(app, rowWidget, appsView); + buildBankRow(app, rowWidget, appsView, isOffline); appsBoxLayout->addWidget(rowWidget, 1); } } diff --git a/views/device/DeviceWidget.h b/views/device/DeviceWidget.h index b817e49..709d616 100644 --- a/views/device/DeviceWidget.h +++ b/views/device/DeviceWidget.h @@ -15,7 +15,7 @@ public: private: const DeviceInfo m_device; - void buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent); + void buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline = false); void setOpenBanksSettings(QPushButton *button, const ApplicationInfo &app); }; diff --git a/views/widget/loader/ErrorWindow.cpp b/views/widget/loader/ErrorWindow.cpp index 904cdd8..1e06124 100644 --- a/views/widget/loader/ErrorWindow.cpp +++ b/views/widget/loader/ErrorWindow.cpp @@ -4,7 +4,7 @@ #include #include -ErrorWindow::ErrorWindow(const QString &errorMessage, QWidget *parent) : QWidget(parent) { +ErrorWindow::ErrorWindow(const QString &errorMessage, bool showRetry, QWidget *parent) : QWidget(parent) { setWindowTitle("Ошибка"); resize(300, 150); @@ -14,6 +14,12 @@ ErrorWindow::ErrorWindow(const QString &errorMessage, QWidget *parent) : QWidget label->setWordWrap(true); layout->addWidget(label); + if (showRetry) { + QPushButton *retryButton = new QPushButton("Повторить"); + layout->addWidget(retryButton); + connect(retryButton, &QPushButton::clicked, this, &ErrorWindow::retryRequested); + } + QPushButton *closeButton = new QPushButton("Закрыть"); layout->addWidget(closeButton); diff --git a/views/widget/loader/ErrorWindow.h b/views/widget/loader/ErrorWindow.h index 324d0a3..4c89874 100644 --- a/views/widget/loader/ErrorWindow.h +++ b/views/widget/loader/ErrorWindow.h @@ -6,5 +6,8 @@ class ErrorWindow final : public QWidget { Q_OBJECT public: - explicit ErrorWindow(const QString &errorMessage, QWidget *parent = nullptr); + explicit ErrorWindow(const QString &errorMessage, bool showRetry = false, QWidget *parent = nullptr); + +signals: + void retryRequested(); };