diff --git a/android/black/CommonScript.cpp b/android/black/CommonScript.cpp index c7a93ef..5ab4053 100644 --- a/android/black/CommonScript.cpp +++ b/android/black/CommonScript.cpp @@ -8,7 +8,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/ApplicationDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/BankProfileDAO.h" #include "net/NetworkService.h" namespace Black { @@ -108,7 +109,7 @@ bool CommonScript::runAppAndGoToHomeScreen( } } -void CommonScript::postAccountsData(const QList> &accounts) { +void CommonScript::postAccountsData(const QList> &accounts) { if (accounts.isEmpty()) { return; } @@ -117,15 +118,15 @@ void CommonScript::postAccountsData(const QList> &accou auto *worker = new NetworkService; QJsonArray list; - for (const QPair &pair : accounts) { + for (const QPair &pair : accounts) { QJsonObject obj; - AccountInfo account = pair.first; + MaterialInfo account = pair.first; obj["appName"] = account.appCode; obj["lastNumbers"] = account.lastNumbers; obj["amount"] = account.amount; obj["description"] = account.description; obj["currency"] = account.currency; - obj["status"] = accountStatusToString(account.status); + obj["status"] = materialStatusToString(account.status); list.append(obj); } @@ -139,7 +140,7 @@ void CommonScript::postAccountsData(const QList> &accou thread->start(); } -void CommonScript::postNewTransactionData(const AccountInfo &account, const TransactionInfo &transaction) { +void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) { auto *thread = new QThread; auto *worker = new NetworkService; @@ -170,7 +171,7 @@ void CommonScript::postNewTransactionData(const AccountInfo &account, const Tran } void CommonScript::updateTransactionData( - const AccountInfo &account, + const MaterialInfo &account, const int transactionId, const TransactionStatus status, const QString &oldDesc, @@ -197,7 +198,7 @@ void CommonScript::updateTransactionData( } void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) { - const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, appCode); + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) { qWarning() << "[Black] createBankProfileIfNeeded: skipping — fullName or phone is empty"; @@ -224,6 +225,8 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr } phone.prepend('+'); + const DeviceInfo device = DeviceDAO::getDeviceById(deviceId); + QJsonObject profileBody; profileBody["first_name"] = parts.value(0); profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString(""); @@ -232,6 +235,7 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr profileBody["phone_number"] = phone; profileBody["balance"] = QString::number(qRound64(app.amount * 100)); profileBody["state"] = (app.status == "active") ? "enabled" : "disabled"; + profileBody["device_id"] = device.apiId; profileBody["currency"] = currencyCode; NetworkService ns; diff --git a/android/black/CommonScript.h b/android/black/CommonScript.h index 98d1afb..1949cea 100644 --- a/android/black/CommonScript.h +++ b/android/black/CommonScript.h @@ -34,12 +34,12 @@ protected: int &counter, int width, int height ); - void postAccountsData(const QList> &accounts); + void postAccountsData(const QList> &accounts); - void postNewTransactionData(const AccountInfo &account, const TransactionInfo &transaction); + void postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction); void updateTransactionData( - const AccountInfo &account, + const MaterialInfo &account, int transactionId, TransactionStatus status, const QString &oldDesc, diff --git a/android/black/GetCardInfoScript.cpp b/android/black/GetCardInfoScript.cpp index b23f4b7..f493f87 100644 --- a/android/black/GetCardInfoScript.cpp +++ b/android/black/GetCardInfoScript.cpp @@ -6,8 +6,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "net/NetworkService.h" @@ -25,8 +25,8 @@ GetCardInfoScript::GetCardInfoScript( GetCardInfoScript::~GetCardInfoScript() = default; void GetCardInfoScript::doStart() { - const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; @@ -41,7 +41,7 @@ void GetCardInfoScript::doStart() { // Если не на домашнем экране — перезапускаем if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[Black::GetCardInfo] Not on home screen, restarting app..."; - if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode; qWarning() << m_error; @@ -55,7 +55,7 @@ void GetCardInfoScript::doStart() { // 1. Парсим баланс с домашнего экрана const double balance = xmlScreenParser.parseBalanceFromHomeScreen(); qDebug() << "[Black::GetCardInfo] balance from home:" << balance; - ApplicationDAO::updateAmount(app.id, balance); + BankProfileDAO::updateAmount(app.id, balance); // 2. Нажимаем "Tu CVU" на домашнем экране Node cvuButton = xmlScreenParser.findButtonNode("Tu CVU", false); @@ -104,24 +104,24 @@ void GetCardInfoScript::doStart() { // 5. Сохраняем аккаунт в БД (lastNumbers = последние 4 цифры CVU) const QString lastNumbers = cvuNumber.right(4); - AccountInfo account; - account.deviceId = m_deviceId; + MaterialInfo account; + account.deviceId = device.id; account.appCode = m_appCode; account.lastNumbers = lastNumbers; account.cardNumber = cvuNumber; account.amount = balance; account.currency = "USD"; account.description = "CVU"; - account.status = AccountStatus::Active; + account.status = MaterialStatus::Active; account.updateTime = QDateTime::currentDateTimeUtc(); - if (!AccountDAO::upsertAccount(account)) { + if (!MaterialDAO::upsertAccount(account)) { qWarning() << "[Black::GetCardInfo] Failed to upsert account"; } // 6. Отправляем материал на сервер (если ещё нет material_id) - const AccountInfo savedAccount = AccountDAO::findAppAccount(m_deviceId, m_appCode, lastNumbers); - const ApplicationInfo updatedApp = ApplicationDAO::getApplication(m_deviceId, m_appCode); + const MaterialInfo savedAccount = MaterialDAO::findAppAccount(device.id, m_appCode, lastNumbers); + const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode); if (savedAccount.id != -1 && !updatedApp.bankProfileId.isEmpty()) { if (savedAccount.materialId.isEmpty()) { diff --git a/android/black/GetCardInfoScript.h b/android/black/GetCardInfoScript.h index 978e5b2..af4c7d5 100644 --- a/android/black/GetCardInfoScript.h +++ b/android/black/GetCardInfoScript.h @@ -19,7 +19,7 @@ protected: void doStart() override; private: - const QString m_deviceId; + QString m_deviceId; const QString m_appCode; QString m_error; }; diff --git a/android/black/GetLastTransactionsScript.cpp b/android/black/GetLastTransactionsScript.cpp index 87f09d9..14c76ad 100644 --- a/android/black/GetLastTransactionsScript.cpp +++ b/android/black/GetLastTransactionsScript.cpp @@ -4,8 +4,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/TransactionDAO.h" #include "AppLogger.h" @@ -23,8 +23,8 @@ GetLastTransactionsScript::GetLastTransactionsScript( GetLastTransactionsScript::~GetLastTransactionsScript() = default; void GetLastTransactionsScript::doStart() { - const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; @@ -38,7 +38,7 @@ void GetLastTransactionsScript::doStart() { // Если не на домашнем экране — перезапускаем приложение if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[Black::GetLastTransactions] Not on home screen, restarting app..."; - if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + m_deviceId; qWarning() << m_error; @@ -113,7 +113,7 @@ void GetLastTransactionsScript::doStart() { // Находим accountId для сохранения транзакций int accountId = -1; - const QList accounts = AccountDAO::getAccounts(m_deviceId, m_appCode); + const QList accounts = MaterialDAO::getAccounts(device.id, m_appCode); if (!accounts.isEmpty()) { accountId = accounts.first().id; qDebug() << "[Black::GetLastTransactions] Using accountId:" << accountId; diff --git a/android/black/GetLastTransactionsScript.h b/android/black/GetLastTransactionsScript.h index 248e9a5..c0a7698 100644 --- a/android/black/GetLastTransactionsScript.h +++ b/android/black/GetLastTransactionsScript.h @@ -19,7 +19,7 @@ protected: void doStart() override; private: - const QString m_deviceId; + QString m_deviceId; const QString m_appCode; QString m_error; }; diff --git a/android/black/GetProfileInfoScript.cpp b/android/black/GetProfileInfoScript.cpp index ac86346..28ded8a 100644 --- a/android/black/GetProfileInfoScript.cpp +++ b/android/black/GetProfileInfoScript.cpp @@ -5,8 +5,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "net/NetworkService.h" @@ -24,8 +24,8 @@ GetProfileInfoScript::GetProfileInfoScript( GetProfileInfoScript::~GetProfileInfoScript() = default; void GetProfileInfoScript::doStart() { - const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; @@ -40,7 +40,7 @@ void GetProfileInfoScript::doStart() { // Если не на домашнем экране — перезапускаем if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[Black::GetProfileInfo] Not on home screen, restarting app..."; - if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode; qWarning() << m_error; @@ -67,7 +67,7 @@ void GetProfileInfoScript::doStart() { qDebug() << "[Black::GetProfileInfo] fullName from home:" << fullName << "balance:" << balance; // Сохраняем баланс - ApplicationDAO::updateAmount(app.id, balance); + BankProfileDAO::updateAmount(app.id, balance); AdbUtils::makeTap(m_deviceId, holaNode.x(), holaNode.y()); QThread::msleep(2000); @@ -155,7 +155,7 @@ void GetProfileInfoScript::doStart() { << "fullName:" << parsedFullName; // 7. Сохраняем в БД - if (!ApplicationDAO::updateProfileInfo(app.id, parsedFullName, phone, email)) { + if (!BankProfileDAO::updateProfileInfo(app.id, parsedFullName, phone, email)) { qWarning() << "[Black::GetProfileInfo] Failed to save profile info to DB"; } @@ -163,8 +163,8 @@ void GetProfileInfoScript::doStart() { createBankProfileIfNeeded(m_deviceId, m_appCode); // 8. Отправляем данные аккаунтов на сервер если нет material_id - const QList allAccounts = AccountDAO::getAccounts(m_deviceId, m_appCode); - QList> accountsToPost; + const QList allAccounts = MaterialDAO::getAccounts(device.id, m_appCode); + QList> accountsToPost; for (const auto &acc : allAccounts) { if (acc.materialId.isEmpty()) { accountsToPost.append({acc, Node()}); diff --git a/android/black/GetProfileInfoScript.h b/android/black/GetProfileInfoScript.h index 94b4883..26a76b4 100644 --- a/android/black/GetProfileInfoScript.h +++ b/android/black/GetProfileInfoScript.h @@ -19,7 +19,7 @@ protected: void doStart() override; private: - const QString m_deviceId; + QString m_deviceId; const QString m_appCode; QString m_error; }; diff --git a/android/black/LoginAndCheckAccountsScript.cpp b/android/black/LoginAndCheckAccountsScript.cpp index 790b80a..dd79508 100644 --- a/android/black/LoginAndCheckAccountsScript.cpp +++ b/android/black/LoginAndCheckAccountsScript.cpp @@ -3,7 +3,7 @@ #include #include "adb/AdbUtils.h" -#include "dao/ApplicationDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "AppLogger.h" #include "GetProfileInfoScript.h" @@ -22,8 +22,9 @@ LoginAndCheckAccountsScript::LoginAndCheckAccountsScript( LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default; void LoginAndCheckAccountsScript::doStart() { - const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + // m_deviceId = ADB serial, device.id = android_id (для БД) + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; @@ -34,26 +35,20 @@ void LoginAndCheckAccountsScript::doStart() { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode; qWarning() << m_error; AppLogger::log("black/LoginAndCheck", m_deviceId, m_error, AdbUtils::captureScreenshotBytes(m_deviceId), "FETCH_PROFILE"); - if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) { - qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode; - } + BankProfileDAO::updateComment(app.id, + "Не прошла проверка " + QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm:ss")); AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; } - // PIN-код прошёл - if (!ApplicationDAO::updatePinCodeStatus(app.id, "yes", "")) { - qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode; - } - // Проверяем профиль и аккаунты (приложение уже на home screen) GetProfileInfoScript profileScript(m_deviceId, m_appCode); profileScript.start(); diff --git a/android/black/LoginAndCheckAccountsScript.h b/android/black/LoginAndCheckAccountsScript.h index a3a9f0c..6bb0dae 100644 --- a/android/black/LoginAndCheckAccountsScript.h +++ b/android/black/LoginAndCheckAccountsScript.h @@ -19,7 +19,7 @@ protected: void doStart() override; private: - const QString m_deviceId; + QString m_deviceId; const QString m_appCode; QString m_error; }; diff --git a/android/black/PayByCardScript.cpp b/android/black/PayByCardScript.cpp index b3b7d52..bba8758 100644 --- a/android/black/PayByCardScript.cpp +++ b/android/black/PayByCardScript.cpp @@ -4,7 +4,7 @@ #include #include "adb/AdbUtils.h" -#include "dao/ApplicationDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/TransactionDAO.h" #include "AppLogger.h" @@ -12,7 +12,7 @@ namespace Black { PayByCardScript::PayByCardScript( - AccountInfo account, + MaterialInfo account, QString cardNumber, double amount, QObject *parent @@ -26,7 +26,7 @@ PayByCardScript::~PayByCardScript() = default; void PayByCardScript::doStart() { const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode); + const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_account.deviceId; @@ -35,7 +35,7 @@ void PayByCardScript::doStart() { return; } - makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight); + makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight); emit finishedWithResult(m_error); } @@ -50,7 +50,7 @@ void PayByCardScript::makePayment( // 0. Убеждаемся что на домашнем экране if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[Black::PayByCard] Not on home screen, restarting app..."; - const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode); + const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode); if (!runAppAndGoToHomeScreen(deviceId, app.package, pinCode, width, height)) { m_error = "Cannot reach home screen: " + deviceId; qWarning() << m_error; diff --git a/android/black/PayByCardScript.h b/android/black/PayByCardScript.h index db20b97..66e4577 100644 --- a/android/black/PayByCardScript.h +++ b/android/black/PayByCardScript.h @@ -8,7 +8,7 @@ class PayByCardScript final : public CommonScript { public: PayByCardScript( - AccountInfo account, + MaterialInfo account, QString cardNumber, double amount, QObject *parent = nullptr @@ -20,7 +20,7 @@ protected: void doStart() override; private: - const AccountInfo m_account; + const MaterialInfo m_account; const QString m_cardNumber; const double m_amount; QString m_error; diff --git a/android/black/ScreenXmlParser.h b/android/black/ScreenXmlParser.h index cc9ade0..535b819 100644 --- a/android/black/ScreenXmlParser.h +++ b/android/black/ScreenXmlParser.h @@ -3,7 +3,7 @@ #include #include #include "android/xml/Node.h" -#include "db/AccountInfo.h" +#include "db/MaterialInfo.h" #include "db/TransactionInfo.h" namespace Black { diff --git a/android/ozon/CommonScript.cpp b/android/ozon/CommonScript.cpp index dc899cd..c71ce21 100644 --- a/android/ozon/CommonScript.cpp +++ b/android/ozon/CommonScript.cpp @@ -8,7 +8,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/ApplicationDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/BankProfileDAO.h" #include "net/NetworkService.h" namespace Ozon { @@ -113,11 +114,18 @@ bool CommonScript::runAppAndGoToHomeScreen( continue; } - // Проверяем на рекламный bottom sheet (ипотека и т.п.) + // Проверяем на рекламный 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); + // Если touch_outside не помог — жмём назад + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (Node stillAd = xmlScreenParser.tryToFindAdBanner(); !stillAd.isEmpty()) { + qDebug() << "[runAppAndGoToHomeScreen] Ad still visible, pressing back..."; + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + } continue; } @@ -187,7 +195,7 @@ bool CommonScript::goToAllProducts( } bool CommonScript::findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers) { - QList > accounts = xmlScreenParser.parseAccountsInfo(); + QList > accounts = xmlScreenParser.parseAccountsInfo(); for (auto &[account, node]: accounts) { // FIXME будем считать что числа последние везде уникальные if (accountNumbers == account.lastNumbers) { @@ -201,7 +209,7 @@ bool CommonScript::findAndTapOnAccount(const QString &deviceId, const QString &a } -void CommonScript::postAccountsData(const QList > &accounts) { +void CommonScript::postAccountsData(const QList > &accounts) { if (accounts.isEmpty()) { return; } @@ -210,15 +218,15 @@ void CommonScript::postAccountsData(const QList > &acco auto *worker = new NetworkService; QJsonArray list; - for (const QPair &pair: accounts) { + for (const QPair &pair: accounts) { QJsonObject obj; - AccountInfo account = pair.first; + MaterialInfo account = pair.first; obj["appName"] = account.appCode; obj["lastNumbers"] = account.lastNumbers; obj["amount"] = account.amount; obj["description"] = account.description; obj["currency"] = account.currency; - obj["status"] = accountStatusToString(account.status); + obj["status"] = materialStatusToString(account.status); list.append(obj); } @@ -233,7 +241,7 @@ void CommonScript::postAccountsData(const QList > &acco thread->start(); } -void CommonScript::postNewTransactionData(const AccountInfo &account, const TransactionInfo &transaction) { +void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) { auto *thread = new QThread; auto *worker = new NetworkService; @@ -265,7 +273,7 @@ void CommonScript::postNewTransactionData(const AccountInfo &account, const Tran } void CommonScript::updateTransactionData( - const AccountInfo &account, + const MaterialInfo &account, const int transactionId, const TransactionStatus status, const QString &oldDesc, @@ -293,7 +301,7 @@ void CommonScript::updateTransactionData( } void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) { - const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, appCode); + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) { qWarning() << "[Ozon] createBankProfileIfNeeded: skipping — fullName or phone is empty"; @@ -320,6 +328,8 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr } phone.prepend('+'); + const DeviceInfo device = DeviceDAO::getDeviceById(deviceId); + QJsonObject profileBody; profileBody["first_name"] = parts.value(0); profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString(""); @@ -328,6 +338,7 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr profileBody["phone_number"] = phone; profileBody["balance"] = QString::number(qRound64(app.amount * 100)); profileBody["state"] = (app.status == "active") ? "enabled" : "disabled"; + profileBody["device_id"] = device.apiId; profileBody["currency"] = currencyCode; NetworkService ns; diff --git a/android/ozon/CommonScript.h b/android/ozon/CommonScript.h index eef6b9b..3e962e4 100644 --- a/android/ozon/CommonScript.h +++ b/android/ozon/CommonScript.h @@ -27,12 +27,12 @@ protected: bool findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers); - void postAccountsData(const QList > &accounts); + void postAccountsData(const QList > &accounts); - void postNewTransactionData(const AccountInfo &account, const TransactionInfo &transaction); + void postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction); void updateTransactionData( - const AccountInfo &account, + const MaterialInfo &account, int transactionId, TransactionStatus status, const QString &oldDesc, diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp index 0cbe17f..56092eb 100644 --- a/android/ozon/GetCardInfoScript.cpp +++ b/android/ozon/GetCardInfoScript.cpp @@ -7,8 +7,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/SettingsDAO.h" #include "net/NetworkService.h" @@ -30,8 +30,8 @@ GetCardInfoScript::GetCardInfoScript( GetCardInfoScript::~GetCardInfoScript() = default; void GetCardInfoScript::doStart() { - const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; @@ -57,7 +57,7 @@ void GetCardInfoScript::doStart() { // Если не на домашнем экране — перезапускаем приложение if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[GetCardInfo] Not on home screen, restarting app..."; - if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + m_deviceId; qWarning() << m_error; @@ -97,7 +97,7 @@ void GetCardInfoScript::doStart() { const QString fullName = xmlScreenParser.parseProfileFullName(); const QString phone = xmlScreenParser.parseProfilePhone(); qDebug() << "[GetCardInfo] Profile parsed - fullName:" << fullName << "phone:" << phone; - ApplicationDAO::updateProfileInfo(app.id, fullName, phone); + BankProfileDAO::updateProfileInfo(app.id, fullName, phone); } else { qWarning() << "[GetCardInfo] Profile screen not reached, skipping profile parse"; } @@ -113,7 +113,7 @@ void GetCardInfoScript::doStart() { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[GetCardInfo] Not on home screen before card search, restarting..."; - if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen before card search: " + m_deviceId; qWarning() << m_error; @@ -129,7 +129,7 @@ void GetCardInfoScript::doStart() { } // Ждём пока все карты подгрузятся — до 5 попыток - QList cards; + QList cards; for (int attempt = 0; attempt < 5; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); cards = xmlScreenParser.parseCardsOnHomeScreen(); @@ -149,12 +149,12 @@ void GetCardInfoScript::doStart() { qDebug() << "[GetCardInfo] Found" << cards.size() << "card(s) on home screen"; // Апсёртим аккаунты для всех найденных карт - for (AccountInfo &card : cards) { - card.deviceId = m_deviceId; + for (MaterialInfo &card : cards) { + card.deviceId = device.id; card.appCode = m_appCode; card.updateTime = QDateTime::currentDateTimeUtc(); - card.status = AccountStatus::Active; - if (!AccountDAO::upsertAccount(card)) { + card.status = MaterialStatus::Active; + if (!MaterialDAO::upsertAccount(card)) { qWarning() << "[GetCardInfo] Failed to upsert account for card" << card.lastNumbers; } else { qDebug() << "[GetCardInfo] Upserted account:" << card.lastNumbers @@ -165,7 +165,7 @@ void GetCardInfoScript::doStart() { // Сохраняем баланс домашнего экрана в application const double totalAmount = cards.isEmpty() ? 0.0 : cards[0].amount; if (totalAmount > 0.0) { - ApplicationDAO::updateAmount(app.id, totalAmount); + BankProfileDAO::updateAmount(app.id, totalAmount); } // Синхронная отправка на сервер (NetworkService блокируется через QEventLoop внутри) @@ -188,7 +188,7 @@ void GetCardInfoScript::doStart() { // 2. Отправляем банковский профиль (используем свежие данные из БД — имя/телефон уже спарсены) { - const ApplicationInfo freshApp = ApplicationDAO::getApplication(m_deviceId, m_appCode); + const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode); const QStringList parts = freshApp.fullName.split(' ', Qt::SkipEmptyParts); // ISO 4217 numeric currency codes @@ -207,6 +207,7 @@ void GetCardInfoScript::doStart() { profileBody["phone_number"] = phone; profileBody["balance"] = QString::number(qRound64(totalAmount * 100)); profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled"; + profileBody["device_id"] = device.apiId; profileBody["currency"] = currencyCode; ns.setPayload(QJsonDocument(profileBody).toJson()); @@ -214,7 +215,7 @@ void GetCardInfoScript::doStart() { ns.addExtra("app_id", freshApp.id); ns.postBankProfile(); // Проверяем что bankProfileId сохранился - const ApplicationInfo afterPost = ApplicationDAO::getApplication(m_deviceId, m_appCode); + const BankProfileInfo afterPost = BankProfileDAO::getApplication(device.id, m_appCode); if (afterPost.bankProfileId.isEmpty()) { const QString err = "postBankProfile failed for " + m_deviceId; qWarning() << "[GetCardInfo]" << err; @@ -233,7 +234,7 @@ void GetCardInfoScript::doStart() { // Для каждой карты переходим на её экран и получаем полный номер через OCR for (int i = 0; i < cards.size(); ++i) { - const AccountInfo &card = cards[i]; + const MaterialInfo &card = cards[i]; // Если передан конкретный lastNumbers — обрабатываем только его if (!m_lastNumbers.isEmpty() && card.lastNumbers != m_lastNumbers) continue; @@ -241,7 +242,7 @@ void GetCardInfoScript::doStart() { qDebug() << "[GetCardInfo] Getting card number for" << card.lastNumbers; // Если номер карты уже есть в БД — сумма уже обновлена через upsert, навигация не нужна - AccountInfo existing = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers); + MaterialInfo existing = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); if (existing.id != -1 && !existing.cardNumber.isEmpty()) { qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "already has number, skipping"; continue; @@ -339,16 +340,16 @@ void GetCardInfoScript::doStart() { if (!cardNumber.isEmpty()) { qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "full number:" << cardNumber; // Получаем id аккаунта из БД и сохраняем номер карты - AccountInfo saved = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers); + MaterialInfo saved = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); if (saved.id != -1) { - AccountDAO::updateCardNumber(saved.id, cardNumber); + MaterialDAO::updateCardNumber(saved.id, cardNumber); } } else { const QString err = "Card number not found for " + card.lastNumbers; qWarning() << "[GetCardInfo]" << err; - AccountInfo saved = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers); + MaterialInfo saved = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); if (saved.id != -1) { - AccountDAO::updateAccountById(saved.id, accountStatusToString(AccountStatus::Off), + MaterialDAO::updateAccountById(saved.id, materialStatusToString(MaterialStatus::Off), saved.description, saved.currency); } AppLogger::log("ozon/GetCardInfo", m_deviceId, err, @@ -365,10 +366,10 @@ void GetCardInfoScript::doStart() { } // 3. Отправляем материалы (карты) — после OCR, когда номера карт уже в БД - const ApplicationInfo updatedApp = ApplicationDAO::getApplication(m_deviceId, m_appCode); + const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode); if (!updatedApp.bankProfileId.isEmpty()) { - for (const AccountInfo &card : cards) { - const AccountInfo dbCard = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers); + for (const MaterialInfo &card : cards) { + const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); if (dbCard.cardNumber.isEmpty()) continue; if (!dbCard.materialId.isEmpty()) { qDebug() << "[GetCardInfo] material already exists for card" << dbCard.lastNumbers << ", skipping"; @@ -379,7 +380,7 @@ void GetCardInfoScript::doStart() { materialBody["bank_profile_id"] = updatedApp.bankProfileId; materialBody["card_number"] = dbCard.cardNumber; materialBody["name"] = updatedApp.fullName; - materialBody["state"] = accountStatusToString(dbCard.status) == "active" ? "enabled" : "disabled"; + materialBody["state"] = materialStatusToString(dbCard.status) == "active" ? "enabled" : "disabled"; ns.setPayload(QJsonDocument(materialBody).toJson()); ns.addExtra("account_id", dbCard.id); ns.postMaterial(); diff --git a/android/ozon/GetCardInfoScript.h b/android/ozon/GetCardInfoScript.h index 1d4deda..b671535 100644 --- a/android/ozon/GetCardInfoScript.h +++ b/android/ozon/GetCardInfoScript.h @@ -23,7 +23,7 @@ protected: void doStart() override; private: - const QString m_deviceId; + QString m_deviceId; const QString m_appCode; const QString m_lastNumbers; QString m_error; diff --git a/android/ozon/GetLastTransactionsScript.cpp b/android/ozon/GetLastTransactionsScript.cpp index eedb281..be38ee9 100644 --- a/android/ozon/GetLastTransactionsScript.cpp +++ b/android/ozon/GetLastTransactionsScript.cpp @@ -9,8 +9,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/TransactionDAO.h" #include "time/DateUtils.h" @@ -33,7 +33,6 @@ static QString recognizeTextFromImage(const QByteArray &pngData) { return {}; } - // Convert to grayscale for better OCR accuracy Pix *gray = pixConvertRGBToGray(pix, 0.0f, 0.0f, 0.0f); pixDestroy(&pix); if (!gray) { @@ -44,8 +43,6 @@ static QString recognizeTextFromImage(const QByteArray &pngData) { } api->SetImage(gray); - // adb screencap PNGs lack DPI metadata; set 300 DPI so Tesseract - // interprets character sizes correctly api->SetSourceResolution(300); char *text = api->GetUTF8Text(); QString result = QString::fromUtf8(text).trimmed(); @@ -58,12 +55,9 @@ static QString recognizeTextFromImage(const QByteArray &pngData) { } static QString extractTransactionId(const QString &ocrText) { - // Ищем по слову "операции" — OCR часто искажает "ID" перед ним - // \S{1,3} вместо точного "ID" покрывает любые OCR-ошибки (1О, 1Ю, ID, 10 и т.д.) QRegularExpression reId(R"(\S{1,3}\s+операции\s*=?\s*([A-Za-zА-Яа-яЁё0-9]+))"); if (auto m = reId.match(ocrText); m.hasMatch()) return m.captured(1); - // "№ Ф-2026-25235663" QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))"); if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1); @@ -76,16 +70,24 @@ GetLastTransactionsScript::GetLastTransactionsScript( QString deviceId, QString appCode, int maxCount, + double searchAmount, + QDateTime searchTime, QObject *parent ) : CommonScript(parent), - m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)), m_maxCount(maxCount) { + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)), + m_maxCount(maxCount), + m_searchAmount(searchAmount), + m_searchTime(std::move(searchTime)) { } GetLastTransactionsScript::~GetLastTransactionsScript() = default; void GetLastTransactionsScript::doStart() { - const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); + if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; @@ -94,22 +96,49 @@ void GetLastTransactionsScript::doStart() { return; } + if (!navigateToTransactionList(device, app)) { + emit finishedWithResult(m_error); + return; + } + + // Находим accountId + int accountId = -1; + const QList accounts = MaterialDAO::getAccounts(device.id, m_appCode); + if (!accounts.isEmpty()) { + accountId = accounts.first().id; + } else { + qWarning() << "[GetLastTransactions] No account found for" << device.id << m_appCode; + } + + // Выбор режима: поиск конкретной транзакции или сканирование последних + if (m_searchAmount > 0) { + searchTransaction(device, accountId); + } else { + scanRecentTransactions(device, accountId); + } + + emit finishedWithResult(m_error); +} + +bool GetLastTransactionsScript::navigateToTransactionList(const DeviceInfo &device, const BankProfileInfo &app) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - // Закрываем рекламный bottom sheet, если открыт + // Закрываем рекламный 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)); + if (Node stillAd = xmlScreenParser.tryToFindAdBanner(); !stillAd.isEmpty()) { + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + } } 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, + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + m_deviceId; qWarning() << m_error; @@ -117,8 +146,7 @@ void GetLastTransactionsScript::doStart() { AdbUtils::captureScreenshotBytes(m_deviceId), "FETCH_TRANSACTIONS"); AdbUtils::tryToKillApplication(m_deviceId, app.package); - emit finishedWithResult(m_error); - return; + return false; } xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); } @@ -128,13 +156,10 @@ void GetLastTransactionsScript::doStart() { QThread::msleep(500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - // Скроллим вниз до кнопки "Все" и тапаем по ней - // Кнопка "Все" на домашнем экране находится у границы навбара (y ≈ 2190), - // поэтому сначала скроллим вниз, чтобы она поднялась выше навигационной панели - const int navBarTop = device.screenHeight - (device.screenHeight / 10); // ~90% высоты экрана + // Скроллим до "Все" и тапаем + const int navBarTop = device.screenHeight - (device.screenHeight / 10); Node allBtn; for (int i = 0; i < 10; ++i) { - // Сначала скроллим, потом ищем const int x = device.screenWidth / 2; const int offset = device.screenHeight / 4; AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - offset, x, offset); @@ -142,74 +167,159 @@ void GetLastTransactionsScript::doStart() { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); allBtn = xmlScreenParser.findButtonNode("Все", false); - if (!allBtn.isEmpty() && allBtn.y() > 0 && allBtn.y() < navBarTop) { - break; - } + if (!allBtn.isEmpty() && allBtn.y() > 0 && allBtn.y() < navBarTop) break; allBtn = {}; - qDebug() << "[GetLastTransactions] 'Все' not found above navbar, attempt" << i + 1 << "/ 10"; } if (allBtn.isEmpty()) { - m_error = "Button 'Все' not found after scrolling: " + m_deviceId; + m_error = "Button 'Все' not found: " + m_deviceId; qWarning() << m_error; - emit finishedWithResult(m_error); - return; + return false; } - qDebug() << "[GetLastTransactions] Found 'Все' at" << allBtn.x() << allBtn.y(); AdbUtils::makeTap(m_deviceId, allBtn.x(), allBtn.y()); QThread::msleep(2000); - // Ждём загрузки экрана "Операции" - bool loaded = false; + // Ждём экран "Операции" for (int attempt = 0; attempt < 10; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - if (xmlScreenParser.isTransactionListScreen()) { - loaded = true; - break; - } - qDebug() << "[GetLastTransactions] Waiting for transaction list screen, attempt" << attempt + 1 << "/ 10"; + if (xmlScreenParser.isTransactionListScreen()) return true; QThread::msleep(1500); } - if (!loaded) { - m_error = "Transaction list screen not loaded: " + m_deviceId; - qWarning() << m_error; - emit finishedWithResult(m_error); - return; + m_error = "Transaction list screen not loaded: " + m_deviceId; + qWarning() << m_error; + return false; +} + +void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int accountId) { + const QTimeZone msk("Europe/Moscow"); + const int maxScrolls = 15; + + qDebug() << "[FetchTransactions] Searching: amount=" << m_searchAmount + << "time=" << m_searchTime.toString(Qt::ISODate); + + for (int scroll = 0; scroll < maxScrolls; ++scroll) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + const QList txButtons = xmlScreenParser.findTransactionButtons(20); + + for (const Node &btn : txButtons) { + const TransactionInfo parsed = xmlScreenParser.parseTransactionButtonText(btn.text); + if (qAbs(qAbs(parsed.amount) - qAbs(m_searchAmount)) > 0.01) continue; + + qDebug() << "[FetchTransactions] Amount match:" << parsed.amount + << "at" << btn.x() << btn.y(); + + AdbUtils::makeTap(m_deviceId, btn.x(), btn.y()); + QThread::msleep(2000); + + // Ждём загрузки деталей + TransactionInfo detail; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + detail = xmlScreenParser.parseTransactionDetail(); + if (!detail.bankTime.isEmpty()) break; + if (xmlScreenParser.isTransactionListScreen()) { + AdbUtils::makeTap(m_deviceId, btn.x(), btn.y()); + } + QThread::msleep(1500); + } + + if (detail.bankTime.isEmpty()) { + qWarning() << "[FetchTransactions] Could not load detail, going back"; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + continue; + } + + // Парсим время + QString dtStr = detail.bankTime; + for (int ci = 0; ci < dtStr.size(); ++ci) { + const QChar ch = dtStr.at(ci); + if (ch != ' ' && ch.category() == QChar::Separator_Space) dtStr[ci] = ' '; + } + + QDateTime txTime = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm"); + if (!txTime.isValid()) txTime = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm"); + if (txTime.isValid()) txTime.setTimeZone(msk); + + // Проверяем ±1 час + if (m_searchTime.isValid() && txTime.isValid()) { + const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC())); + if (diffSecs > 3600) { + qDebug() << "[FetchTransactions] Time mismatch: tx=" << dtStr + << "diff=" << diffSecs << "sec, skipping"; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + continue; + } + } + + // Нашли! + qDebug() << "[FetchTransactions] FOUND transaction:" + << "amount=" << parsed.amount << "time=" << dtStr + << "name=" << detail.name; + + // Сохраняем + if (accountId != -1) { + TransactionInfo tx; + tx.accountId = accountId; + tx.amount = parsed.amount; + tx.bankName = m_appCode; + tx.bankTime = dtStr; + tx.status = TransactionStatus::Complete; + tx.timestamp = DateUtils::getUtcNow(); + if (txTime.isValid()) tx.completeTime = txTime; + if (!detail.name.isEmpty()) tx.name = detail.name; + if (!detail.description.isEmpty()) tx.description = detail.description; + if (!detail.phone.isEmpty()) { + QString cleanPhone; + for (const QChar &ch : detail.phone) + if (ch.isDigit()) cleanPhone.append(ch); + tx.phone = cleanPhone; + tx.type = TransactionType::Phone; + } + TransactionDAO::insertTransactionIfNotExists(tx); + } + + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + return; // успех — m_error пустой + } + + // Скроллим вниз + const int x = device.screenWidth / 2; + const int offset = device.screenHeight / 4; + AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - offset, x, offset); + QThread::msleep(1000); } - // Ждём появления транзакций (могут подгружаться) + m_error = "Transaction not found: amount=" + QString::number(m_searchAmount, 'f', 2) + + " time=" + m_searchTime.toString(Qt::ISODate); + qWarning() << "[FetchTransactions]" << m_error; +} + +void GetLastTransactionsScript::scanRecentTransactions(const DeviceInfo &device, int accountId) { + const QTimeZone msk("Europe/Moscow"); + const int navBarTop = device.screenHeight - (device.screenHeight / 10); + + // Ждём появления транзакций QList txButtons; for (int attempt = 0; attempt < 5; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); txButtons = xmlScreenParser.findTransactionButtons(8); if (!txButtons.isEmpty()) break; - qDebug() << "[GetLastTransactions] No transactions yet, attempt" << attempt + 1 << "/ 5"; QThread::msleep(1500); } qDebug() << "[GetLastTransactions] Found" << txButtons.size() << "transaction buttons"; - // Находим accountId для сохранения транзакций - int accountId = -1; - const QList accounts = AccountDAO::getAccounts(m_deviceId, m_appCode); - if (!accounts.isEmpty()) { - accountId = accounts.first().id; - qDebug() << "[GetLastTransactions] Using accountId:" << accountId; - } else { - qWarning() << "[GetLastTransactions] No account found for" << m_deviceId << m_appCode; - } - - // Для каждой транзакции: клик → берём дату из деталей → назад QList transactions; for (int i = 0; i < txButtons.size(); ++i) { const Node &btn = txButtons[i]; - // Если кнопка внизу экрана (за навбаром) — скроллим до неё - const int navBarTop = device.screenHeight - (device.screenHeight / 10); + // Скроллим до кнопки если за экраном if (btn.y() <= 0 || btn.y() >= navBarTop) { - qDebug() << "[GetLastTransactions] Transaction" << i + 1 << "is off-screen (y=" << btn.y() << "), scrolling..."; for (int s = 0; s < 5; ++s) { const int x = device.screenWidth / 2; const int offset = device.screenHeight / 4; @@ -217,121 +327,84 @@ void GetLastTransactionsScript::doStart() { QThread::msleep(1000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); txButtons = xmlScreenParser.findTransactionButtons(8); - if (i < txButtons.size() && txButtons[i].y() > 0 && txButtons[i].y() < navBarTop) { - break; - } - } - if (i >= txButtons.size()) { - qWarning() << "[GetLastTransactions] Transaction" << i + 1 << "not found after scrolling"; - break; + if (i < txButtons.size() && txButtons[i].y() > 0 && txButtons[i].y() < navBarTop) break; } + if (i >= txButtons.size()) break; } + const Node &visibleBtn = txButtons[i]; TransactionInfo tx = xmlScreenParser.parseTransactionButtonText(visibleBtn.text); - qDebug() << "[GetLastTransactions] Tapping transaction" << i + 1 << ":" << visibleBtn.text.left(50); AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y()); QThread::msleep(2000); - // Ждём загрузки деталей — ищем дату в шапке + // Ждём загрузки деталей TransactionInfo detail; for (int attempt = 0; attempt < 10; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); detail = xmlScreenParser.parseTransactionDetail(); if (!detail.bankTime.isEmpty()) break; - - // Если всё ещё на экране операций — тап не сработал, повторяем if (xmlScreenParser.isTransactionListScreen()) { - qDebug() << "[GetLastTransactions] Still on transaction list, re-tapping" << i + 1; AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y()); } - - qDebug() << "[GetLastTransactions] Detail not loaded, attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); } - tx.bankName = "ozon"; + + tx.bankName = m_appCode; tx.bankTime = detail.bankTime; tx.status = TransactionStatus::Complete; tx.timestamp = DateUtils::getUtcNow(); if (!detail.name.isEmpty()) tx.name = detail.name; if (!detail.phone.isEmpty()) { - // Оставляем только цифры QString cleanPhone; - for (const QChar &ch : detail.phone) { + for (const QChar &ch : detail.phone) if (ch.isDigit()) cleanPhone.append(ch); - } tx.phone = cleanPhone; tx.type = TransactionType::Phone; } - // Чистим bankTime от unicode символов: заменяем все не-ASCII пробелы на обычные + // Парсим время if (!tx.bankTime.isEmpty()) { QString dtStr = tx.bankTime; for (int ci = 0; ci < dtStr.size(); ++ci) { const QChar ch = dtStr.at(ci); - if (ch != ' ' && ch.category() == QChar::Separator_Space) { - dtStr[ci] = ' '; - } + if (ch != ' ' && ch.category() == QChar::Separator_Space) dtStr[ci] = ' '; } tx.bankTime = dtStr; - const QTimeZone msk("Europe/Moscow"); QDateTime parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm"); - if (!parsed.isValid()) { - parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm"); - } + if (!parsed.isValid()) parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm"); if (parsed.isValid()) { parsed.setTimeZone(msk); tx.completeTime = parsed; } } - qDebug() << "[GetLastTransactions]" - << "date:" << tx.bankTime - << "name:" << tx.description - << "sender:" << tx.name - << "phone:" << tx.phone - << "amount:" << (tx.amount >= 0 ? "+" : "") + QString::number(tx.amount, 'f', 2); - - // Если задан лимит по количеству — проверяем счётчик - // Иначе фильтруем по 24 часам - if (m_maxCount > 0) { - if (transactions.size() + 1 >= m_maxCount) { - // Эта транзакция последняя — сохраним и выйдем после обработки - } - } else if (tx.completeTime.isValid()) { + // Фильтр по времени (24ч) если нет maxCount + if (m_maxCount <= 0 && tx.completeTime.isValid()) { const qint64 ageSecs = tx.completeTime.toUTC().secsTo(QDateTime::currentDateTimeUtc()); if (ageSecs > 24 * 3600) { - qDebug() << "[GetLastTransactions] Transaction older than 24h, stopping"; AdbUtils::goBack(m_deviceId); QThread::msleep(1000); break; } } - // Сохраняем в БД если не существует - // Берём первый аккаунт для этого устройства и приложения + // Сохраняем if (accountId != -1) { tx.accountId = accountId; if (TransactionDAO::insertTransactionIfNotExists(tx)) { - qDebug() << "[GetLastTransactions] Saved transaction to DB"; - - // Делаем скриншот документа транзакции + // Скриншот чека Node docBtn = xmlScreenParser.findButtonNode("Документы", false); if (!docBtn.isEmpty()) { - qDebug() << "[GetLastTransactions] Tapping 'Документы'..."; AdbUtils::makeTap(m_deviceId, docBtn.x(), docBtn.y()); QThread::msleep(2000); - // Ждём появления кнопки "Поделиться" bool docLoaded = false; for (int da = 0; da < 20; ++da) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - Node shareBtn = xmlScreenParser.findNodeByContentDesc("Поделиться"); - if (!shareBtn.isEmpty()) { - docLoaded = true; - break; + if (!xmlScreenParser.findNodeByContentDesc("Поделиться").isEmpty()) { + docLoaded = true; break; } - qDebug() << "[GetLastTransactions] Waiting for 'Поделиться', attempt" << da + 1 << "/ 20"; QThread::msleep(1500); } @@ -346,70 +419,42 @@ void GetLastTransactionsScript::doStart() { accountId, tx.bankName, tx.bankTime, tx.amount); if (saved.id != -1) { TransactionDAO::updateReceiptImage(saved.id, screenshot); - qDebug() << "[GetLastTransactions] Receipt saved to DB, txId:" << saved.id; - - // OCR: распознаём текст и достаём ID операции const QString ocrText = recognizeTextFromImage(screenshot); if (!ocrText.isEmpty()) { - qDebug() << "[GetLastTransactions] OCR result, txId:" << saved.id - << "\n--- OCR TEXT START ---\n" << ocrText - << "\n--- OCR TEXT END ---"; - qDebug() << "[GetLastTransactions] OCR hex:" - << ocrText.left(200).toUtf8().toHex(' '); const QString txId = extractTransactionId(ocrText); if (!txId.isEmpty()) { TransactionDAO::updateBankTrExternalId(saved.id, txId); - qDebug() << "[GetLastTransactions] Extracted transaction ID:" << txId; - } else { - qWarning() << "[GetLastTransactions] Could not extract transaction ID from OCR text"; } - } else { - qWarning() << "[GetLastTransactions] OCR returned empty text"; } } - } else { - qWarning() << "[GetLastTransactions] Screenshot is empty"; } - } else { - qWarning() << "[GetLastTransactions] 'Поделиться' not found, skipping receipt"; } - - // Возвращаемся из документов в детали транзакции AdbUtils::goBack(m_deviceId); QThread::msleep(1000); } - } else { - qDebug() << "[GetLastTransactions] Transaction already exists or error"; } } transactions.append(tx); - // Если достигнут лимит по количеству — выходим if (m_maxCount > 0 && transactions.size() >= m_maxCount) { - qDebug() << "[GetLastTransactions] Reached maxCount limit:" << m_maxCount; AdbUtils::goBack(m_deviceId); QThread::msleep(1000); break; } - // Возвращаемся в список AdbUtils::goBack(m_deviceId); QThread::msleep(1500); - // Ждём возврата на экран списка for (int attempt = 0; attempt < 5; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); if (xmlScreenParser.isTransactionListScreen()) break; QThread::msleep(1000); } - // Обновляем кнопки (после возврата координаты могут измениться) xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); txButtons = xmlScreenParser.findTransactionButtons(8); } - - emit finishedWithResult(m_error); } } // namespace Ozon diff --git a/android/ozon/GetLastTransactionsScript.h b/android/ozon/GetLastTransactionsScript.h index 0c54b22..686889d 100644 --- a/android/ozon/GetLastTransactionsScript.h +++ b/android/ozon/GetLastTransactionsScript.h @@ -1,5 +1,8 @@ #pragma once #include "CommonScript.h" +#include +#include "db/DeviceInfo.h" +#include "db/BankProfileInfo.h" namespace Ozon { @@ -11,6 +14,8 @@ public: QString deviceId, QString appCode, int maxCount = 0, + double searchAmount = 0, + QDateTime searchTime = {}, QObject *parent = nullptr ); @@ -20,9 +25,20 @@ protected: void doStart() override; private: - const QString m_deviceId; + // Навигация до экрана "Операции", возвращает true если успешно + bool navigateToTransactionList(const DeviceInfo &device, const BankProfileInfo &app); + + // Сканирование последних транзакций (для UI: 24ч или maxCount) + void scanRecentTransactions(const DeviceInfo &device, int accountId); + + // Поиск конкретной транзакции по сумме и времени (для задач FETCH_TRANSACTIONS) + void searchTransaction(const DeviceInfo &device, int accountId); + + QString m_deviceId; const QString m_appCode; - const int m_maxCount; // 0 = без лимита (фильтр по 24ч), >0 = только N последних + const int m_maxCount; // 0 = фильтр по 24ч, >0 = N последних + const double m_searchAmount; // сумма в рублях (0 = не искать конкретную) + const QDateTime m_searchTime; // примерное время (UTC) QString m_error; }; diff --git a/android/ozon/GetProfileInfoScript.cpp b/android/ozon/GetProfileInfoScript.cpp index 21e116a..d941d82 100644 --- a/android/ozon/GetProfileInfoScript.cpp +++ b/android/ozon/GetProfileInfoScript.cpp @@ -1,14 +1,15 @@ #include "GetProfileInfoScript.h" #include +#include #include #include #include #include #include "adb/AdbUtils.h" -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "net/NetworkService.h" #include "AppLogger.h" @@ -26,8 +27,8 @@ GetProfileInfoScript::GetProfileInfoScript( GetProfileInfoScript::~GetProfileInfoScript() = default; void GetProfileInfoScript::doStart() { - const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; @@ -53,7 +54,7 @@ void GetProfileInfoScript::doStart() { // Если мы не на домашнем экране — перезапускаем приложение и вводим пин-код if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[GetProfileInfo] Not on home screen, restarting app..."; - if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode; qWarning() << m_error; @@ -107,17 +108,18 @@ void GetProfileInfoScript::doStart() { // Извлекаем полное имя и телефон const QString fullName = xmlScreenParser.parseProfileFullName(); - const QString phone = xmlScreenParser.parseProfilePhone().simplified().remove(' '); + QString phone = xmlScreenParser.parseProfilePhone(); + phone.remove(QRegularExpression(R"(\s+)")); // убираем все виды пробелов (включая \u00A0) qDebug() << "[GetProfileInfo] fullName:" << fullName << "phone:" << phone; - if (!ApplicationDAO::updateProfileInfo(app.id, fullName, phone)) { + if (!BankProfileDAO::updateProfileInfo(app.id, fullName, phone)) { m_error = "Failed to save profile info to DB: " + m_deviceId; qWarning() << m_error; } // Отправляем bank_profile на сервер - const ApplicationInfo freshApp = ApplicationDAO::getApplication(m_deviceId, m_appCode); + const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode); const QMap currencyCodes = { {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32} @@ -133,6 +135,7 @@ void GetProfileInfoScript::doStart() { profileBody["phone_number"] = phone; profileBody["balance"] = QString::number(qRound64(freshApp.amount * 100)); profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled"; + profileBody["device_id"] = device.apiId; profileBody["currency"] = currencyCode; NetworkService ns; @@ -142,7 +145,7 @@ void GetProfileInfoScript::doStart() { if (freshApp.bankProfileId.isEmpty()) { ns.addExtra("app_id", freshApp.id); ns.postBankProfile(); - const ApplicationInfo afterPost = ApplicationDAO::getApplication(m_deviceId, m_appCode); + const BankProfileInfo afterPost = BankProfileDAO::getApplication(device.id, m_appCode); if (afterPost.bankProfileId.isEmpty()) { const QString err = "postBankProfile failed for " + m_deviceId; qWarning() << "[GetProfileInfo]" << err; @@ -159,8 +162,8 @@ void GetProfileInfoScript::doStart() { } // Отправляем данные аккаунтов на сервер если нет material_id - const QList allAccounts = AccountDAO::getAccounts(m_deviceId, m_appCode); - QList> accountsToPost; + const QList allAccounts = MaterialDAO::getAccounts(device.id, m_appCode); + QList> accountsToPost; for (const auto &acc : allAccounts) { if (acc.materialId.isEmpty()) { accountsToPost.append({acc, Node()}); diff --git a/android/ozon/GetProfileInfoScript.h b/android/ozon/GetProfileInfoScript.h index 2b24158..eb715a1 100644 --- a/android/ozon/GetProfileInfoScript.h +++ b/android/ozon/GetProfileInfoScript.h @@ -19,7 +19,7 @@ protected: void doStart() override; private: - const QString m_deviceId; + QString m_deviceId; const QString m_appCode; QString m_error; }; diff --git a/android/ozon/LoginAndCheckAccountsScript.cpp b/android/ozon/LoginAndCheckAccountsScript.cpp index 045f6a4..45733f6 100644 --- a/android/ozon/LoginAndCheckAccountsScript.cpp +++ b/android/ozon/LoginAndCheckAccountsScript.cpp @@ -3,8 +3,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "AppLogger.h" #include "GetCardInfoScript.h" @@ -14,18 +14,21 @@ namespace Ozon { LoginAndCheckAccountsScript::LoginAndCheckAccountsScript( QString deviceId, QString appCode, - QObject *parent + QObject *parent, + bool fetchProfileOnly ) : CommonScript(parent), m_deviceId(std::move(deviceId)), - m_appCode(std::move(appCode)) { + m_appCode(std::move(appCode)), + m_fetchProfileOnly(fetchProfileOnly) { } LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default; void LoginAndCheckAccountsScript::doStart() { - const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode); + // m_deviceId = ADB serial, device.id = android_id (для БД) + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; @@ -36,46 +39,45 @@ void LoginAndCheckAccountsScript::doStart() { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode; qWarning() << m_error; AppLogger::log("ozon/LoginAndCheck", m_deviceId, m_error, AdbUtils::captureScreenshotBytes(m_deviceId), "FETCH_PROFILE"); - if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) { - qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode; - } + BankProfileDAO::updateComment(app.id, + "Не прошла проверка " + QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm:ss")); AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; } else { - if (!ApplicationDAO::updatePinCodeStatus(app.id, "yes", "")) { - qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode; - } - // Создаём bank profile если ещё не создан - createBankProfileIfNeeded(m_deviceId, m_appCode); + createBankProfileIfNeeded(device.id, m_appCode); - if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) { - QList > accounts = xmlScreenParser.parseAccountsInfo(); + if (goToAllProducts(m_deviceId, device.screenWidth, device.screenHeight)) { + QList > accounts = xmlScreenParser.parseAccountsInfo(); for (auto &[account, node]: accounts) { - account.deviceId = m_deviceId; + account.deviceId = device.id; account.appCode = m_appCode; - if (!AccountDAO::upsertAccount(account)) { + if (!MaterialDAO::upsertAccount(account)) { m_error = "Not updated: " + account.lastNumbers; qCritical() << m_error; } } - postAccountsData(accounts); + if (!m_fetchProfileOnly) { + postAccountsData(accounts); + } } else { qWarning() << "Cant go to all products: " << m_deviceId << " " << m_appCode; } - // Запускаем сканирование баланса, данных карт и отправку на сервер (синхронно) - GetCardInfoScript cardScript(m_deviceId, m_appCode, ""); - cardScript.start(); + if (!m_fetchProfileOnly) { + // Запускаем сканирование баланса, данных карт и отправку на сервер (синхронно) + GetCardInfoScript cardScript(m_deviceId, m_appCode, ""); + cardScript.start(); + } } emit finishedWithResult(m_error); diff --git a/android/ozon/LoginAndCheckAccountsScript.h b/android/ozon/LoginAndCheckAccountsScript.h index 52a654c..f3a537d 100644 --- a/android/ozon/LoginAndCheckAccountsScript.h +++ b/android/ozon/LoginAndCheckAccountsScript.h @@ -12,15 +12,17 @@ public: explicit LoginAndCheckAccountsScript( QString deviceId, QString appCode, - QObject *parent = nullptr + QObject *parent = nullptr, + bool fetchProfileOnly = false ); protected: void doStart() override; private: - const QString m_deviceId; + QString m_deviceId; const QString m_appCode; + const bool m_fetchProfileOnly; QString m_error; }; diff --git a/android/ozon/PayByCardScript.cpp b/android/ozon/PayByCardScript.cpp index e118670..a544c3a 100644 --- a/android/ozon/PayByCardScript.cpp +++ b/android/ozon/PayByCardScript.cpp @@ -10,8 +10,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/TransactionDAO.h" #include "time/DateUtils.h" @@ -59,7 +59,7 @@ static QString extractTransactionId(const QString &ocrText) { namespace Ozon { PayByCardScript::PayByCardScript( - AccountInfo account, + MaterialInfo account, QString cardNumber, const double amount, QObject *parent @@ -72,7 +72,7 @@ PayByCardScript::~PayByCardScript() = default; void PayByCardScript::doStart() { const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode); + const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_account.deviceId; @@ -94,11 +94,11 @@ void PayByCardScript::doStart() { // Если не на домашнем экране — перезапускаем if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[PayByCard] Not on home screen, restarting app..."; - if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + device.id; qWarning() << m_error; - AdbUtils::tryToKillApplication(device.id, app.package); + AdbUtils::tryToKillApplication(device.adbSerial, app.package); emit finishedWithResult(m_error); return; } @@ -126,7 +126,7 @@ void PayByCardScript::doStart() { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); } - makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight); + makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight); emit finishedWithResult(m_error); } diff --git a/android/ozon/PayByCardScript.h b/android/ozon/PayByCardScript.h index c613bae..1998ea7 100644 --- a/android/ozon/PayByCardScript.h +++ b/android/ozon/PayByCardScript.h @@ -10,7 +10,7 @@ public: ~PayByCardScript() override; PayByCardScript( - AccountInfo account, + MaterialInfo account, QString cardNumber, double amount, QObject *parent = nullptr @@ -20,7 +20,7 @@ protected: void doStart() override; private: - const AccountInfo m_account; + const MaterialInfo m_account; const QString m_cardNumber; const double m_amount; QString m_error; diff --git a/android/ozon/PayByPhoneScript.cpp b/android/ozon/PayByPhoneScript.cpp index 682d1d9..0d63afc 100644 --- a/android/ozon/PayByPhoneScript.cpp +++ b/android/ozon/PayByPhoneScript.cpp @@ -10,8 +10,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/TransactionDAO.h" #include "time/DateUtils.h" @@ -59,7 +59,7 @@ static QString extractTransactionId(const QString &ocrText) { namespace Ozon { PayByPhoneScript::PayByPhoneScript( - AccountInfo account, + MaterialInfo account, QString phone, QString bankName, const double amount, @@ -73,7 +73,7 @@ PayByPhoneScript::~PayByPhoneScript() = default; void PayByPhoneScript::doStart() { const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); - const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode); + const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_account.deviceId; @@ -95,11 +95,11 @@ void PayByPhoneScript::doStart() { // Если не на домашнем экране — перезапускаем if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[PayByPhone] Not on home screen, restarting app..."; - if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, + if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + device.id; qWarning() << m_error; - AdbUtils::tryToKillApplication(device.id, app.package); + AdbUtils::tryToKillApplication(device.adbSerial, app.package); emit finishedWithResult(m_error); return; } @@ -127,7 +127,7 @@ void PayByPhoneScript::doStart() { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter)); } - makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight); + makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight); emit finishedWithResult(m_error); } diff --git a/android/ozon/PayByPhoneScript.h b/android/ozon/PayByPhoneScript.h index d8c2113..9d5bc56 100644 --- a/android/ozon/PayByPhoneScript.h +++ b/android/ozon/PayByPhoneScript.h @@ -10,7 +10,7 @@ public: ~PayByPhoneScript() override; PayByPhoneScript( - AccountInfo account, + MaterialInfo account, QString phone, QString bankName, double amount, @@ -21,7 +21,7 @@ protected: void doStart() override; private: - const AccountInfo m_account; + const MaterialInfo m_account; const QString m_phone; const QString m_bankName; const double m_amount; diff --git a/android/ozon/ScreenXmlParser.cpp b/android/ozon/ScreenXmlParser.cpp index 55d6025..6d512b2 100644 --- a/android/ozon/ScreenXmlParser.cpp +++ b/android/ozon/ScreenXmlParser.cpp @@ -13,7 +13,7 @@ #include "adb/AdbUtils.h" #include "android/xml/Node.h" -#include "db/AccountInfo.h" +#include "db/MaterialInfo.h" #include "db/TransactionInfo.h" #include "time/DateUtils.h" @@ -358,7 +358,7 @@ Node ScreenXmlParser::findCardButtonByLastNumbers(const QString &lastNumbers) { return {}; } -QList ScreenXmlParser::parseCardsOnHomeScreen() { +QList ScreenXmlParser::parseCardsOnHomeScreen() { // "Основной счёт X ₽" — общая кнопка с балансом для всех карт. // Карты — отдельные кнопки с текстом из 4 цифр (могут быть любые non-breaking spaces). // Сумма из "Основной счёт" проставляется на все карты. @@ -388,13 +388,13 @@ QList ScreenXmlParser::parseCardsOnHomeScreen() { qWarning() << "[parseCards] Balance button not found or amount=0"; // 2. Собираем все кнопки карт (текст заканчивается на 4 цифры) - QList cards; + 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; + MaterialInfo acc; acc.lastNumbers = t.right(4); acc.amount = commonAmount; acc.currency = "RUB"; @@ -1329,8 +1329,8 @@ TransactionInfo ScreenXmlParser::parseTransactionInfo(int screenWidth, int scree } -QList > ScreenXmlParser::parseAccountsInfo() { - QList > accounts; +QList > ScreenXmlParser::parseAccountsInfo() { + QList > accounts; for (const Node &node: m_nodes) { const QString text = node.text; @@ -1343,7 +1343,7 @@ QList > ScreenXmlParser::parseAccountsInfo() { continue; } - AccountInfo info; + MaterialInfo info; info.lastNumbers = match.captured(1).remove(" "); info.amount = match.captured(2).remove(" ").toFloat(); info.description = text.mid(0, text.indexOf("Номер карты")); @@ -1495,7 +1495,7 @@ void ScreenXmlParser::test() { // Номер карты с последними цифрами QDomElement root = doc.documentElement(); parseAndSaveXml(doc.toString()); - // for (const AccountInfo &info: parseAccountsInfo()) { + // for (const MaterialInfo &info: parseAccountsInfo()) { // qDebug().noquote().nospace() << convertAccountToString(info); // } // TransactionInfo info = parseExpandedInfo(root); diff --git a/android/ozon/ScreenXmlParser.h b/android/ozon/ScreenXmlParser.h index 23828fc..877c31f 100644 --- a/android/ozon/ScreenXmlParser.h +++ b/android/ozon/ScreenXmlParser.h @@ -3,7 +3,7 @@ #include #include #include "android/xml/Node.h" -#include "db/AccountInfo.h" +#include "db/MaterialInfo.h" #include "db/TransactionInfo.h" namespace Ozon { @@ -50,9 +50,9 @@ public: // 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 + // Returns all cards visible on the home screen as MaterialInfo stubs // (lastNumbers, description, amount filled; id=-1, cardNumber empty) - QList parseCardsOnHomeScreen(); + QList parseCardsOnHomeScreen(); bool isProfileScreen(); @@ -76,7 +76,7 @@ public: QList parseTodayAndYesterdayHistory(); - QList > parseAccountsInfo(); + QList > parseAccountsInfo(); // Finds a TextView with full card number (format "XXXX XXXX XXXX XXXX") QString findFullCardNumber(); diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index 97d118e..50af47e 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -81,178 +81,143 @@ QString DatabaseManager::connectionName() { } -static bool columnExists(QSqlQuery &q, const QString &table, const QString &column) { - // Валидация: допускаем только [a-zA-Z0-9_] чтобы исключить SQL injection - static const QRegularExpression valid("^[a-zA-Z_][a-zA-Z0-9_]*$"); - if (!valid.match(table).hasMatch() || !valid.match(column).hasMatch()) { - qCritical() << "[DB] columnExists: invalid identifier" << table << column; - return false; - } - - q.exec(QString("SELECT COUNT(*) FROM pragma_table_info('%1') WHERE name='%2'") - .arg(table, column)); - return q.next() && q.value(0).toInt() > 0; -} - void DatabaseManager::initSchema() { QSqlDatabase db = database(); QSqlQuery q(db); - // Читаем текущую версию схемы - q.exec("PRAGMA user_version"); - const int version = q.next() ? q.value(0).toInt() : 0; + db.transaction(); - // ── Версия 1: полная схема ───────────────────────────────────────────────── - if (version < 1) { - db.transaction(); + q.exec(R"(CREATE TABLE IF NOT EXISTS devices ( + id TEXT PRIMARY KEY, + api_id TEXT, + adb_serial 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 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 bank_profiles ( + 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, + phone TEXT DEFAULT '', + email TEXT DEFAULT '', + full_name TEXT DEFAULT '', + bank_profile_id TEXT DEFAULT '', + currency TEXT DEFAULT '', + amount REAL DEFAULT 0, + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, + UNIQUE (device_id, code) + ))"); - 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, - phone TEXT DEFAULT '', - email TEXT DEFAULT '', - full_name TEXT DEFAULT '', - bank_profile_id TEXT DEFAULT '', - currency TEXT DEFAULT '', - amount REAL DEFAULT 0, - FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, - UNIQUE (device_id, code) - ))"); + q.exec(R"(CREATE TABLE IF NOT EXISTS materials ( + 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', + card_number TEXT DEFAULT '', + material_id TEXT DEFAULT '', + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, + UNIQUE (device_id, app_code, last_numbers) + ))"); - 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', - card_number TEXT DEFAULT '', - material_id TEXT DEFAULT '', - FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, - UNIQUE (device_id, app_code, last_numbers) - ))"); + 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, + receipt_image BLOB, + FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE + ))"); - 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, - receipt_image BLOB, - FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE - ))"); + q.exec(R"(CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER, + external_id TEXT, + 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, + json TEXT DEFAULT '', + sent_to_server INTEGER DEFAULT 0, + screenshot BLOB, + FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE, + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE + ))"); - q.exec(R"(CREATE TABLE IF NOT EXISTS events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - account_id INTEGER, - external_id TEXT, - 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, - json TEXT DEFAULT '', - sent_to_server INTEGER DEFAULT 0, - FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE, - FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE - ))"); + q.exec(R"(CREATE TABLE IF NOT EXISTS banks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alias TEXT UNIQUE, + name TEXT, + nspk_name TEXT + ))"); - q.exec(R"(CREATE TABLE IF NOT EXISTS banks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - alias TEXT UNIQUE, - name TEXT, - nspk_name TEXT - ))"); + 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(R"(CREATE TABLE IF NOT EXISTS app_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - source TEXT NOT NULL DEFAULT '', - device_id TEXT NOT NULL DEFAULT '', - message TEXT NOT NULL, - screenshot BLOB, - timestamp TEXT NOT NULL - ))"); + q.exec("CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC)"); - q.exec("CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC)"); + q.exec(R"(CREATE TABLE IF NOT EXISTS general_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '', + message TEXT NOT NULL, + timestamp TEXT NOT NULL + ))"); - q.exec(R"(CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT - ))"); + q.exec("CREATE INDEX IF NOT EXISTS idx_general_logs_id ON general_logs (id DESC)"); - db.commit(); - q.exec("PRAGMA user_version = 1"); - qDebug() << "[DB] Migration 1 applied"; - } + q.exec(R"(CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT + ))"); - // ── Версия 2: добавляем email в applications ───────────────────────────── - if (version < 2) { - db.transaction(); - q.exec("ALTER TABLE applications ADD COLUMN email TEXT DEFAULT ''"); - db.commit(); - q.exec("PRAGMA user_version = 2"); - qDebug() << "[DB] Migration 2 applied"; - } - - // ── Версия 3: таблица general_logs для всех Qt-логов ──────────────────── - if (version < 3) { - db.transaction(); - q.exec(R"(CREATE TABLE IF NOT EXISTS general_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - level TEXT NOT NULL DEFAULT '', - source TEXT NOT NULL DEFAULT '', - message TEXT NOT NULL, - timestamp TEXT NOT NULL - ))"); - q.exec("CREATE INDEX IF NOT EXISTS idx_general_logs_id ON general_logs (id DESC)"); - db.commit(); - q.exec("PRAGMA user_version = 3"); - qDebug() << "[DB] Migration 3 applied"; - } + db.commit(); } void DatabaseManager::closeConnection() { diff --git a/database/dao/AppLogDAO.cpp b/database/dao/AppLogDAO.cpp index 0375054..990f58a 100644 --- a/database/dao/AppLogDAO.cpp +++ b/database/dao/AppLogDAO.cpp @@ -68,3 +68,30 @@ QByteArray AppLogDAO::getScreenshot(int id) { if (!q.exec() || !q.next()) return {}; return q.value(0).toByteArray(); } + +QList AppLogDAO::getLogsWithScreenshots(int limit) { + QList result; + QSqlQuery q(DatabaseManager::instance().database()); + q.prepare(R"( + SELECT id, source, device_id, message, screenshot, timestamp + FROM app_logs + ORDER BY id DESC + LIMIT :limit + )"); + q.bindValue(":limit", limit); + if (!q.exec()) { + qWarning() << "[AppLogDAO] getLogsWithScreenshots failed:" << q.lastError().text(); + return result; + } + while (q.next()) { + AppLogEntry e; + e.id = q.value("id").toInt(); + e.source = q.value("source").toString(); + e.deviceId = q.value("device_id").toString(); + e.message = q.value("message").toString(); + e.screenshot = q.value("screenshot").toByteArray(); + e.timestamp = DateUtils::fromUtcString(q.value("timestamp").toString()); + result.append(e); + } + return result; +} diff --git a/database/dao/AppLogDAO.h b/database/dao/AppLogDAO.h index dec1863..e03bbad 100644 --- a/database/dao/AppLogDAO.h +++ b/database/dao/AppLogDAO.h @@ -14,4 +14,7 @@ public: // Загружает скриншот отдельно по id (по клику в UI) [[nodiscard]] static QByteArray getScreenshot(int id); + + // Все записи со скриншотами (для экспорта) + [[nodiscard]] static QList getLogsWithScreenshots(int limit = 100); }; diff --git a/database/dao/ApplicationDAO.cpp b/database/dao/BankProfileDAO.cpp similarity index 76% rename from database/dao/ApplicationDAO.cpp rename to database/dao/BankProfileDAO.cpp index 156d72e..6aebc91 100644 --- a/database/dao/ApplicationDAO.cpp +++ b/database/dao/BankProfileDAO.cpp @@ -2,15 +2,15 @@ #include #include #include -#include "ApplicationDAO.h" +#include "BankProfileDAO.h" #include "DatabaseManager.h" -#include "db/ApplicationInfo.h" +#include "db/BankProfileInfo.h" -bool ApplicationDAO::upsertApplication(const ApplicationInfo &info) { +bool BankProfileDAO::upsertApplication(const BankProfileInfo &info) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - INSERT INTO applications (id, device_id, pin_code, name, code, package, status, comment, install, pin_code_checked) + INSERT INTO bank_profiles (id, device_id, pin_code, name, code, package, status, comment, install, pin_code_checked) VALUES (:id, :device_id, :pin_code, :name, :code, :package, :status, :comment, :install, :pin_code_checked) ON CONFLICT(device_id, code) DO UPDATE SET @@ -44,11 +44,11 @@ bool ApplicationDAO::upsertApplication(const ApplicationInfo &info) { return true; } -bool ApplicationDAO::updatePinCode(const QString &appId, const QString &pinCode) { +bool BankProfileDAO::updatePinCode(const QString &appId, const QString &pinCode) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - UPDATE applications + UPDATE bank_profiles SET pin_code = :pin_code WHERE id = :id )"); @@ -63,7 +63,7 @@ bool ApplicationDAO::updatePinCode(const QString &appId, const QString &pinCode) return true; } -bool ApplicationDAO::update( +bool BankProfileDAO::update( const int &appId, const QString &pinCode, const QString &comment, @@ -72,7 +72,7 @@ bool ApplicationDAO::update( QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - UPDATE applications + UPDATE bank_profiles SET pin_code = :pin_code, comment = :comment, status = :status WHERE id = :id )"); @@ -89,7 +89,7 @@ bool ApplicationDAO::update( return true; } -bool ApplicationDAO::updatePinCodeStatus( +bool BankProfileDAO::updatePinCodeStatus( const int &appId, const QString &pinCodeStatus, const QString &comment @@ -97,15 +97,14 @@ bool ApplicationDAO::updatePinCodeStatus( QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - UPDATE applications - SET pin_code_checked = :pin_code_checked, comment = :comment, status = :status + UPDATE bank_profiles + SET pin_code_checked = :pin_code_checked, comment = :comment WHERE id = :id )"); query.bindValue(":id", appId); query.bindValue(":comment", comment); query.bindValue(":pin_code_checked", pinCodeStatus); - query.bindValue(":status", pinCodeStatus == "yes" ? "active" : "off"); if (!query.exec()) { qWarning() << "Ошибка при обновлении PIN-кода приложения:" << query.lastError().text(); @@ -115,8 +114,20 @@ bool ApplicationDAO::updatePinCodeStatus( } -ApplicationInfo parseApplication(const QSqlQuery &query) { - ApplicationInfo app; +bool BankProfileDAO::updateComment(const int appId, const QString &comment) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE bank_profiles SET comment = :comment WHERE id = :id"); + query.bindValue(":comment", comment); + query.bindValue(":id", appId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении комментария:" << query.lastError().text(); + return false; + } + return true; +} + +BankProfileInfo parseApplication(const QSqlQuery &query) { + BankProfileInfo app; app.id = query.value("id").toInt(); app.deviceId = query.value("device_id").toString(); app.pinCode = query.value("pin_code").toString(); @@ -136,12 +147,12 @@ ApplicationInfo parseApplication(const QSqlQuery &query) { return app; } -ApplicationInfo ApplicationDAO::getApplication(const QString &deviceId, const QString &appCode) { +BankProfileInfo BankProfileDAO::getApplication(const QString &deviceId, const QString &appCode) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount - FROM applications + FROM bank_profiles WHERE device_id = :device_id AND code = :code LIMIT 1 )"); @@ -160,13 +171,13 @@ ApplicationInfo ApplicationDAO::getApplication(const QString &deviceId, const QS return {}; } -QList ApplicationDAO::getApplicationsByDeviceId(const QString &deviceId) { +QList BankProfileDAO::getApplicationsByDeviceId(const QString &deviceId) { QSqlQuery query(DatabaseManager::instance().database()); - QList list; + QList list; query.prepare(R"( SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount - FROM applications + FROM bank_profiles WHERE device_id = :device_id AND install = 'yes' )"); query.bindValue(":device_id", deviceId); @@ -183,7 +194,30 @@ QList ApplicationDAO::getApplicationsByDeviceId(const QString & return list; } -bool ApplicationDAO::updateProfileInfo( +QList BankProfileDAO::getAllApplicationsByDeviceId(const QString &deviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + QList list; + + query.prepare(R"( + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount + FROM bank_profiles + WHERE device_id = :device_id + )"); + query.bindValue(":device_id", deviceId); + + if (!query.exec()) { + qWarning() << "Ошибка при получении всех приложений по deviceId:" << query.lastError().text(); + return {}; + } + + while (query.next()) { + list.append(parseApplication(query)); + } + + return list; +} + +bool BankProfileDAO::updateProfileInfo( const int &appId, const QString &fullName, const QString &phone, @@ -192,7 +226,7 @@ bool ApplicationDAO::updateProfileInfo( QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - UPDATE applications + UPDATE bank_profiles SET full_name = :full_name, phone = :phone, email = :email WHERE id = :id )"); @@ -209,9 +243,9 @@ bool ApplicationDAO::updateProfileInfo( return true; } -bool ApplicationDAO::updateAmount(const int appId, const double amount) { +bool BankProfileDAO::updateAmount(const int appId, const double amount) { QSqlQuery query(DatabaseManager::instance().database()); - query.prepare("UPDATE applications SET amount = :amount WHERE id = :id"); + query.prepare("UPDATE bank_profiles SET amount = :amount WHERE id = :id"); query.bindValue(":amount", amount); query.bindValue(":id", appId); if (!query.exec()) { @@ -221,9 +255,9 @@ bool ApplicationDAO::updateAmount(const int appId, const double amount) { return true; } -bool ApplicationDAO::updateBankProfileId(const int appId, const QString &bankProfileId) { +bool BankProfileDAO::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.prepare("UPDATE bank_profiles SET bank_profile_id = :bank_profile_id WHERE id = :id"); query.bindValue(":bank_profile_id", bankProfileId); query.bindValue(":id", appId); if (!query.exec()) { @@ -233,17 +267,17 @@ bool ApplicationDAO::updateBankProfileId(const int appId, const QString &bankPro return true; } -QList ApplicationDAO::findAllByBankProfileId(const QString &bankProfileId) { - QList list; +QList BankProfileDAO::findAllByBankProfileId(const QString &bankProfileId) { + QList list; QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount - FROM applications + FROM bank_profiles WHERE bank_profile_id = :bank_profile_id )"); query.bindValue(":bank_profile_id", bankProfileId); if (!query.exec()) { - qWarning() << "Ошибка при поиске applications по bank_profile_id:" << query.lastError().text(); + qWarning() << "Ошибка при поиске bank_profiles по bank_profile_id:" << query.lastError().text(); return list; } while (query.next()) { @@ -252,10 +286,10 @@ QList ApplicationDAO::findAllByBankProfileId(const QString &ban return list; } -bool ApplicationDAO::activateAppsForDevice(const QString &deviceId) { +bool BankProfileDAO::activateAppsForDevice(const QString &deviceId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - UPDATE applications SET status = 'active' + UPDATE bank_profiles SET status = 'active' WHERE device_id = :device_id AND install = 'yes' AND pin_code_checked = 'yes' )"); query.bindValue(":device_id", deviceId); @@ -266,10 +300,10 @@ bool ApplicationDAO::activateAppsForDevice(const QString &deviceId) { return true; } -bool ApplicationDAO::deactivateAppsForDevice(const QString &deviceId) { +bool BankProfileDAO::deactivateAppsForDevice(const QString &deviceId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - UPDATE applications SET status = 'off' + UPDATE bank_profiles SET status = 'off' WHERE device_id = :device_id AND install = 'yes' )"); query.bindValue(":device_id", deviceId); @@ -280,7 +314,7 @@ bool ApplicationDAO::deactivateAppsForDevice(const QString &deviceId) { return true; } -bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet &apps) { +bool BankProfileDAO::checkInstalledApps(const QString &deviceId, const QSet &apps) { QSqlQuery query(DatabaseManager::instance().database()); const QSettings settings("config.ini", QSettings::IniFormat); QStringList banks = settings.value("apps/list", QStringList()).toStringList(); @@ -290,7 +324,7 @@ bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet findAllByBankProfileId(const QString &bankProfileId); + [[nodiscard]] static QList findAllByBankProfileId(const QString &bankProfileId); [[nodiscard]] static bool updateAmount(int appId, double amount); - [[nodiscard]] static ApplicationInfo getApplication(const QString &deviceId, const QString &appCode); + [[nodiscard]] static BankProfileInfo getApplication(const QString &deviceId, const QString &appCode); - [[nodiscard]] static QList getApplicationsByDeviceId(const QString &deviceId); + [[nodiscard]] static QList getApplicationsByDeviceId(const QString &deviceId); + + [[nodiscard]] static QList getAllApplicationsByDeviceId(const QString &deviceId); [[nodiscard]] static bool checkInstalledApps(const QString &deviceId, const QSet &apps); diff --git a/database/dao/DeviceDAO.cpp b/database/dao/DeviceDAO.cpp index 29aed15..4f4c76c 100644 --- a/database/dao/DeviceDAO.cpp +++ b/database/dao/DeviceDAO.cpp @@ -9,6 +9,7 @@ DeviceInfo parseDevice(const QSqlQuery &query) { DeviceInfo device; device.id = query.value("id").toString(); device.apiId = query.value("api_id").toString(); + device.adbSerial = query.value("adb_serial").toString(); device.status = query.value("status").toString(); device.name = query.value("name").toString(); device.android = query.value("android").toString(); @@ -25,7 +26,7 @@ QList DeviceDAO::getAllDevices(const bool online) { QSqlQuery query(DatabaseManager::instance().database()); QString sql = R"( - SELECT id, api_id, status, name, android, screen_width, screen_height, battery, update_time, image + SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image FROM devices )"; if (online) { @@ -71,7 +72,7 @@ DeviceInfo DeviceDAO::getDeviceById(const QString &deviceId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, api_id, status, name, android, screen_width, screen_height, battery, update_time, image + SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image FROM devices WHERE id = :deviceId )"); @@ -89,6 +90,69 @@ DeviceInfo DeviceDAO::getDeviceById(const QString &deviceId) { return {}; } +DeviceInfo DeviceDAO::findByAdbSerial(const QString &adbSerial) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image + FROM devices + WHERE adb_serial = :adb_serial + LIMIT 1 + )"); + query.bindValue(":adb_serial", adbSerial); + + if (!query.exec()) { + qWarning() << "Ошибка при поиске устройства по adb_serial:" << query.lastError().text(); + return {}; + } + + if (query.next()) { + return parseDevice(query); + } + return {}; +} + +DeviceInfo DeviceDAO::findByName(const QString &name) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image + FROM devices + WHERE name = :name + LIMIT 1 + )"); + query.bindValue(":name", name); + + if (!query.exec()) { + qWarning() << "Ошибка при поиске устройства по name:" << query.lastError().text(); + return {}; + } + + if (query.next()) { + return parseDevice(query); + } + return {}; +} + +DeviceInfo DeviceDAO::findUnmatchedDevice() { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image + FROM devices + WHERE api_id IS NOT NULL AND api_id != '' + AND (adb_serial IS NULL OR adb_serial = '') + LIMIT 1 + )"); + + if (!query.exec()) { + qWarning() << "Ошибка при поиске unmatched device:" << query.lastError().text(); + return {}; + } + + if (query.next()) { + return parseDevice(query); + } + return {}; +} + bool DeviceDAO::upsertDevice(const DeviceInfo &device) { QSqlQuery query(DatabaseManager::instance().database()); @@ -107,6 +171,8 @@ bool DeviceDAO::upsertDevice(const DeviceInfo &device) { // Обновление query.prepare(R"( UPDATE devices SET + api_id = CASE WHEN :api_id != '' THEN :api_id ELSE api_id END, + adb_serial = CASE WHEN :adb_serial != '' THEN :adb_serial ELSE adb_serial END, status = :status, name = :name, android = :android, @@ -120,14 +186,16 @@ bool DeviceDAO::upsertDevice(const DeviceInfo &device) { // Вставка query.prepare(R"( INSERT INTO devices ( - id, status, name, android, screen_width, screen_height, battery, update_time + id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time ) VALUES ( - :id, :status, :name, :android, :width, :height, :battery, :update_time + :id, :api_id, :adb_serial, :status, :name, :android, :width, :height, :battery, :update_time ) )"); } query.bindValue(":id", device.id); + query.bindValue(":api_id", device.apiId); + query.bindValue(":adb_serial", device.adbSerial); query.bindValue(":status", device.status); query.bindValue(":name", device.name); query.bindValue(":android", device.android); diff --git a/database/dao/DeviceDAO.h b/database/dao/DeviceDAO.h index fdadb52..c9bbe71 100644 --- a/database/dao/DeviceDAO.h +++ b/database/dao/DeviceDAO.h @@ -6,6 +6,12 @@ class DeviceDAO { public: [[nodiscard]] static DeviceInfo getDeviceById(const QString &deviceId); + [[nodiscard]] static DeviceInfo findByAdbSerial(const QString &adbSerial); + + [[nodiscard]] static DeviceInfo findByName(const QString &name); + + [[nodiscard]] static DeviceInfo findUnmatchedDevice(); + [[nodiscard]] static bool upsertDevice(const DeviceInfo &device); [[nodiscard]] static bool updateApiId(const QString &deviceId, const QString &apiId); diff --git a/database/dao/EventDAO.cpp b/database/dao/EventDAO.cpp index bd8c279..040752b 100644 --- a/database/dao/EventDAO.cpp +++ b/database/dao/EventDAO.cpp @@ -160,7 +160,14 @@ EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) { status, type, comment, json, sent_to_server, update_time, complete_time, timestamp FROM events WHERE status = :status AND device_id = :device_id - ORDER BY timestamp ASC + ORDER BY + CASE type + WHEN 'CREATE_TRANSACTION' THEN 0 + WHEN 'FETCH_TRANSACTIONS' THEN 1 + WHEN 'FETCH_PROFILE' THEN 2 + ELSE 3 + END ASC, + timestamp ASC LIMIT 1 )"); query.bindValue(":status", eventStatusToString(EventStatus::Wait)); @@ -258,3 +265,78 @@ bool EventDAO::existsActiveByExternalId(const QString &externalId) { } return query.next(); } + +bool EventDAO::cancelWaitingByType(const QString &deviceId, EventType type) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE events SET status = :cancelled, comment = 'Cancelled: replaced by new task' + WHERE device_id = :device_id AND type = :type AND status = :wait + )"); + query.bindValue(":cancelled", eventStatusToString(EventStatus::Error)); + query.bindValue(":device_id", deviceId); + query.bindValue(":type", eventTypeToString(type)); + query.bindValue(":wait", eventStatusToString(EventStatus::Wait)); + + if (!query.exec()) { + qWarning() << "Ошибка при cancelWaitingByType:" << query.lastError().text(); + return false; + } + return true; +} + +QList EventDAO::cancelWaitingByBank(const QString &deviceId, const QString &bankName) { + // Сначала получаем задачи которые будем отменять (для отправки на сервер) + QList cancelled; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, account_id, device_id, external_id, amount, phone, bank_name, + status, type, comment, json, sent_to_server, update_time, complete_time, timestamp + FROM events + WHERE device_id = :device_id AND bank_name = :bank_name AND status = :wait + )"); + query.bindValue(":device_id", deviceId); + query.bindValue(":bank_name", bankName); + query.bindValue(":wait", eventStatusToString(EventStatus::Wait)); + if (query.exec()) { + while (query.next()) { + cancelled.append(parseEvent(query)); + } + } + + // Отменяем + query.prepare(R"( + UPDATE events SET status = :error, comment = 'Bank profile disabled' + WHERE device_id = :device_id AND bank_name = :bank_name AND status = :wait + )"); + query.bindValue(":error", eventStatusToString(EventStatus::Error)); + query.bindValue(":device_id", deviceId); + query.bindValue(":bank_name", bankName); + query.bindValue(":wait", eventStatusToString(EventStatus::Wait)); + if (!query.exec()) { + qWarning() << "Ошибка при cancelWaitingByBank:" << query.lastError().text(); + } + + return cancelled; +} + +bool EventDAO::updateScreenshot(int id, const QByteArray &screenshot) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE events SET screenshot = :screenshot WHERE id = :id"); + query.bindValue(":screenshot", screenshot.isEmpty() + ? QVariant(QMetaType(QMetaType::QByteArray)) + : QVariant(screenshot)); + query.bindValue(":id", id); + if (!query.exec()) { + qWarning() << "Ошибка при updateScreenshot:" << query.lastError().text(); + return false; + } + return true; +} + +QByteArray EventDAO::getScreenshot(int id) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("SELECT screenshot FROM events WHERE id = :id"); + query.bindValue(":id", id); + if (!query.exec() || !query.next()) return {}; + return query.value(0).toByteArray(); +} diff --git a/database/dao/EventDAO.h b/database/dao/EventDAO.h index 467d2e8..4520713 100644 --- a/database/dao/EventDAO.h +++ b/database/dao/EventDAO.h @@ -19,6 +19,14 @@ public: [[nodiscard]] static bool existsActiveByExternalId(const QString &externalId); + [[nodiscard]] static bool cancelWaitingByType(const QString &deviceId, EventType type); + + [[nodiscard]] static QList cancelWaitingByBank(const QString &deviceId, const QString &bankName); + + [[nodiscard]] static bool updateScreenshot(int id, const QByteArray &screenshot); + + [[nodiscard]] static QByteArray getScreenshot(int id); + [[nodiscard]] static bool updateEventAmount(int id, double amount); [[nodiscard]] static bool markSentToServer(int id); diff --git a/database/dao/AccountDAO.cpp b/database/dao/MaterialDAO.cpp similarity index 77% rename from database/dao/AccountDAO.cpp rename to database/dao/MaterialDAO.cpp index 45acfad..237fa62 100644 --- a/database/dao/AccountDAO.cpp +++ b/database/dao/MaterialDAO.cpp @@ -1,16 +1,16 @@ #include #include -#include "AccountDAO.h" +#include "MaterialDAO.h" -#include "db/AccountInfo.h" +#include "db/MaterialInfo.h" #include "DatabaseManager.h" #include "time/DateUtils.h" -bool AccountDAO::upsertAccount(const AccountInfo &info) { +bool MaterialDAO::upsertAccount(const MaterialInfo &info) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - INSERT INTO accounts (device_id, app_code, last_numbers, card_number, amount, update_time, status, description, currency) + INSERT INTO materials (device_id, app_code, last_numbers, card_number, amount, update_time, status, description, currency) VALUES (:device_id, :app_code, :last_numbers, :card_number, :amount, :update_time, :status, :description, :currency) ON CONFLICT(device_id, app_code, last_numbers) DO UPDATE SET @@ -31,7 +31,7 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) { query.bindValue(":description", info.description); query.bindValue(":amount", info.amount); query.bindValue(":update_time", DateUtils::toUtcString(info.updateTime)); - query.bindValue(":status", accountStatusToString(info.status)); + query.bindValue(":status", materialStatusToString(info.status)); if (!query.exec()) { qWarning() << "Ошибка при вставке/обновлении account:" << query.lastError().text(); @@ -41,7 +41,7 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) { return true; } -bool AccountDAO::updateAccountById( +bool MaterialDAO::updateAccountById( const int id, const QString &status, const QString &description, @@ -50,7 +50,7 @@ bool AccountDAO::updateAccountById( QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - UPDATE accounts + UPDATE materials SET status = :status, description = :description, last_numbers = :last_numbers, @@ -72,17 +72,17 @@ bool AccountDAO::updateAccountById( return true; } -bool AccountDAO::deleteAccount(const int id) { +bool MaterialDAO::deleteAccount(const int id) { QSqlQuery query(DatabaseManager::instance().database()); - query.prepare("DELETE FROM accounts WHERE id = :id"); + query.prepare("DELETE FROM materials WHERE id = :id"); query.bindValue(":id", id); return query.exec(); } -bool AccountDAO::updateCardNumber(const int accountId, const QString &cardNumber) { +bool MaterialDAO::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 + UPDATE materials SET card_number = :card_number WHERE id = :id )"); query.bindValue(":card_number", cardNumber); query.bindValue(":id", accountId); @@ -93,9 +93,9 @@ bool AccountDAO::updateCardNumber(const int accountId, const QString &cardNumber return true; } -bool AccountDAO::updateMaterialId(const int accountId, const QString &materialId) { +bool MaterialDAO::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.prepare("UPDATE materials SET material_id = :material_id WHERE id = :id"); query.bindValue(":material_id", materialId); query.bindValue(":id", accountId); if (!query.exec()) { @@ -105,8 +105,8 @@ bool AccountDAO::updateMaterialId(const int accountId, const QString &materialId return true; } -AccountInfo parseAccount(const QSqlQuery &query) { - AccountInfo acc; +MaterialInfo parseAccount(const QSqlQuery &query) { + MaterialInfo acc; acc.id = query.value("id").toInt(); acc.deviceId = query.value("device_id").toString(); acc.appCode = query.value("app_code").toString(); @@ -115,19 +115,19 @@ AccountInfo parseAccount(const QSqlQuery &query) { 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()); + acc.status = materialStatusFromString(query.value("status").toString()); acc.description = query.value("description").toString(); acc.currency = query.value("currency").toString(); return acc; } -QList AccountDAO::getAccounts(const QString &deviceId, const QString &appName) { - QList accounts; +QList MaterialDAO::getAccounts(const QString &deviceId, const QString &appName) { + QList accounts; QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency - FROM accounts + FROM materials WHERE device_id = :device_id AND app_code = :app_code )"); @@ -135,7 +135,7 @@ QList AccountDAO::getAccounts(const QString &deviceId, const QStrin query.bindValue(":app_code", appName); if (!query.exec()) { - qWarning() << "Ошибка при получении accounts:" << query.lastError().text(); + qWarning() << "Ошибка при получении materials:" << query.lastError().text(); return accounts; } @@ -146,12 +146,12 @@ QList AccountDAO::getAccounts(const QString &deviceId, const QStrin return accounts; } -AccountInfo AccountDAO::getAccountById(const int accountId) { +MaterialInfo MaterialDAO::getAccountById(const int accountId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency - FROM accounts + FROM materials WHERE id = :id LIMIT 1 )"); @@ -169,20 +169,20 @@ AccountInfo AccountDAO::getAccountById(const int accountId) { return {}; } -QList AccountDAO::getAccountsByAppCode(const QString &appCode) { - QList accounts; +QList MaterialDAO::getAccountsByAppCode(const QString &appCode) { + QList accounts; QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency - FROM accounts + FROM materials WHERE app_code = :app_code )"); query.bindValue(":app_code", appCode); if (!query.exec()) { - qWarning() << "Ошибка при получении accounts по app_code:" << query.lastError().text(); + qWarning() << "Ошибка при получении materials по app_code:" << query.lastError().text(); return accounts; } @@ -193,12 +193,12 @@ QList AccountDAO::getAccountsByAppCode(const QString &appCode) { return accounts; } -AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &appName, const QString &lastNumber) { +MaterialInfo MaterialDAO::findAppAccount(const QString &deviceId, const QString &appName, const QString &lastNumber) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency - FROM accounts + FROM materials WHERE app_code = :appName AND last_numbers = :lastNumber AND device_id = :deviceId LIMIT 1 )"); @@ -219,13 +219,13 @@ AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &a return {}; } -QStringList AccountDAO::getActiveMaterialIds() { +QStringList MaterialDAO::getActiveMaterialIds() { QStringList ids; QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( SELECT a.material_id - FROM accounts a - JOIN applications app ON a.device_id = app.device_id AND a.app_code = app.code + FROM materials a + JOIN bank_profiles app ON a.device_id = app.device_id AND a.app_code = app.code JOIN devices d ON a.device_id = d.id WHERE app.status = 'active' AND a.material_id IS NOT NULL AND a.material_id != '' @@ -243,13 +243,13 @@ QStringList AccountDAO::getActiveMaterialIds() { return ids; } -QStringList AccountDAO::getActiveMaterialIdsExcludingDevices(const QSet &excludeDevices) { +QStringList MaterialDAO::getActiveMaterialIdsExcludingDevices(const QSet &excludeDevices) { const QStringList all = getActiveMaterialIds(); if (excludeDevices.isEmpty()) return all; QStringList filtered; for (const QString &mid : all) { - const AccountInfo acc = findByMaterialId(mid); + const MaterialInfo acc = findByMaterialId(mid); if (!excludeDevices.contains(acc.deviceId)) { filtered << mid; } @@ -257,12 +257,12 @@ QStringList AccountDAO::getActiveMaterialIdsExcludingDevices(const QSet return filtered; } -AccountInfo AccountDAO::findByMaterialId(const QString &materialId) { +MaterialInfo MaterialDAO::findByMaterialId(const QString &materialId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency - FROM accounts + FROM materials WHERE material_id = :material_id LIMIT 1 )"); diff --git a/database/dao/AccountDAO.h b/database/dao/MaterialDAO.h similarity index 58% rename from database/dao/AccountDAO.h rename to database/dao/MaterialDAO.h index 9927a98..7b1ee11 100644 --- a/database/dao/AccountDAO.h +++ b/database/dao/MaterialDAO.h @@ -1,10 +1,10 @@ #pragma once #include -#include "db/AccountInfo.h" +#include "db/MaterialInfo.h" -class AccountDAO { +class MaterialDAO { public: - [[nodiscard]] static bool upsertAccount(const AccountInfo &info); + [[nodiscard]] static bool upsertAccount(const MaterialInfo &info); [[nodiscard]] static bool updateAccountById( int id, @@ -19,19 +19,19 @@ public: [[nodiscard]] static bool updateMaterialId(int accountId, const QString &materialId); - [[nodiscard]] static QList getAccounts(const QString &deviceId, const QString &appName); + [[nodiscard]] static QList getAccounts(const QString &deviceId, const QString &appName); - [[nodiscard]] static AccountInfo getAccountById(int accountId); + [[nodiscard]] static MaterialInfo getAccountById(int accountId); - [[nodiscard]] static QList getAccountsByAppCode(const QString &appCode); + [[nodiscard]] static QList getAccountsByAppCode(const QString &appCode); - [[nodiscard]] static AccountInfo findAppAccount( + [[nodiscard]] static MaterialInfo findAppAccount( const QString &deviceId, const QString &appName, const QString &lastNumber ); - [[nodiscard]] static AccountInfo findByMaterialId(const QString &materialId); + [[nodiscard]] static MaterialInfo findByMaterialId(const QString &materialId); [[nodiscard]] static QStringList getActiveMaterialIds(); diff --git a/docs/api/openapi.json b/docs/api/openapi.json new file mode 100644 index 0000000..cd755b5 --- /dev/null +++ b/docs/api/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"Adb Core Backend","version":"1.0.0"},"paths":{"/api/v1/auth/login":{"post":{"summary":"Login","operationId":"login_api_v1_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequestData"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_LoginResponseData_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/refresh_token":{"post":{"summary":"Login","operationId":"login_api_v1_auth_refresh_token_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenData"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_LoginResponseData_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/current_profile":{"get":{"summary":"Current Profile","operationId":"current_profile_api_v1_profile_current_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_ProfileResponse_"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/profile/fetch_bank_profiles":{"post":{"summary":"Fetch Bank Profiles","operationId":"fetch_bank_profiles_api_v1_profile_fetch_bank_profiles_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_list_BankProfileResponse__"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/profile/fetch_materials":{"post":{"summary":"Fetch Materials","operationId":"fetch_materials_api_v1_profile_fetch_materials_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_list_MaterialResponseModel__"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/profile/bank_profile":{"post":{"summary":"Create Bank Profile","operationId":"create_bank_profile_api_v1_profile_bank_profile_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBankProfile"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_BankProfileResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/profile/bank_profile/{bank_profile_id}":{"patch":{"summary":"Update Bank Profile","operationId":"update_bank_profile_api_v1_profile_bank_profile__bank_profile_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bank_profile_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Bank Profile Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBankProfileModel"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_BankProfileModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Bank Profile","operationId":"delete_bank_profile_api_v1_profile_bank_profile__bank_profile_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bank_profile_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Bank Profile Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/bank_profile/{bank_profile_id}/{is_enable}":{"patch":{"summary":"Unable Or Disable Bank Profile","operationId":"unable_or_disable_bank_profile_api_v1_profile_bank_profile__bank_profile_id___is_enable__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bank_profile_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Bank Profile Id"}},{"name":"is_enable","in":"path","required":true,"schema":{"type":"boolean","title":"Is Enable"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_BankProfileModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/material":{"post":{"summary":"Create Material","operationId":"create_material_api_v1_profile_material_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMaterialRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_MaterialResponseModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/profile/material/{material_id}":{"patch":{"summary":"Update Material","operationId":"update_material_api_v1_profile_material__material_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"material_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Material Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMaterialData"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_MaterialModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Material","operationId":"delete_material_api_v1_profile_material__material_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"material_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Material Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/material/{material_id}/{is_enable}":{"patch":{"summary":"Unable Or Disable Material","operationId":"unable_or_disable_material_api_v1_profile_material__material_id___is_enable__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"material_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Material Id"}},{"name":"is_enable","in":"path","required":true,"schema":{"type":"boolean","title":"Is Enable"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_MaterialModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/desktop/":{"get":{"summary":"Get All Desktops","operationId":"get_all_desktops_api_v1_desktop__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_list_DesktopResponseModel__"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"summary":"Create Desktop","operationId":"create_desktop_api_v1_desktop__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDesktopData"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_DesktopResponseModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/desktop/{desktop_id}":{"patch":{"summary":"Update Desktop","operationId":"update_desktop_api_v1_desktop__desktop_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"desktop_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desktop Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDesktopData"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_DesktopResponseModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Desktop","operationId":"delete_desktop_api_v1_desktop__desktop_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"desktop_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desktop Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/device/{desktop_id}":{"get":{"summary":"Get All Devices By Desktop Id","operationId":"get_all_devices_by_desktop_id_api_v1_device__desktop_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"desktop_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desktop Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_list_DeviceResponseModel__"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/device/":{"post":{"summary":"Create Desktop","operationId":"create_desktop_api_v1_device__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceData"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_DeviceResponseModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/device/{device_id}":{"patch":{"summary":"Update Device","operationId":"update_device_api_v1_device__device_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceData"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_DeviceResponseModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Device","operationId":"delete_device_api_v1_device__device_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/device/add_screenshot/{device_id}":{"post":{"summary":"Add Screenshot","operationId":"add_screenshot_api_v1_device_add_screenshot__device_id__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_add_screenshot_api_v1_device_add_screenshot__device_id__post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_GetDeviceScreenshotModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/device/delete_screenshot/{screenshot_id}":{"delete":{"summary":"Delete Screenshot","operationId":"delete_screenshot_api_v1_device_delete_screenshot__screenshot_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"screenshot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Screenshot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/task/add_task":{"post":{"summary":"Add Task","operationId":"add_task_api_v1_task_add_task_post","requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CreateTransactionTask-Input"},{"$ref":"#/components/schemas/FetchTransactionsTask"},{"$ref":"#/components/schemas/FetchProfileTask"}],"title":"Data","discriminator":{"propertyName":"type","mapping":{"CREATE_TRANSACTION":"#/components/schemas/CreateTransactionTask-Input","FETCH_TRANSACTIONS":"#/components/schemas/FetchTransactionsTask","FETCH_PROFILE":"#/components/schemas/FetchProfileTask"}}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_NoneType_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/task/task_result":{"post":{"summary":"Add Task","operationId":"add_task_api_v1_task_task_result_post","requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CreateTransactionEvent"},{"$ref":"#/components/schemas/FetchTransactionsEvent"},{"$ref":"#/components/schemas/FetchProfileEvent"}],"title":"Data","discriminator":{"propertyName":"type","mapping":{"CREATE_TRANSACTION":"#/components/schemas/CreateTransactionEvent","FETCH_TRANSACTIONS":"#/components/schemas/FetchTransactionsEvent","FETCH_PROFILE":"#/components/schemas/FetchProfileEvent"}}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_NoneType_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/task/get_tasks":{"get":{"summary":"Get Task","operationId":"get_task_api_v1_task_get_tasks_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"materials_id","in":"query","required":true,"schema":{"type":"array","items":{"type":"string"},"title":"Materials Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_list_Annotated_Union_CreateTransactionTask__FetchTransactionsTask__FetchProfileTask___FieldInfo_annotation_NoneType__required_True__discriminator__type_____"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/task/add_bank":{"post":{"summary":"Add Task","operationId":"add_task_api_v1_task_add_bank_post","parameters":[{"name":"bank_name","in":"query","required":true,"schema":{"type":"string","title":"Bank Name"}},{"name":"webhook_url","in":"query","required":true,"schema":{"type":"string","format":"uri","minLength":1,"maxLength":2083,"title":"Webhook Url"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_NoneType_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitoring/log":{"post":{"summary":"Log","operationId":"log_monitoring_log_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_log_monitoring_log_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_BaseLogResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Account":{"properties":{"bank_account_id":{"type":"string","title":"Bank Account Id"},"currency":{"anyOf":[{"$ref":"#/components/schemas/CurrencyEnum"},{"type":"null"}]},"amount":{"type":"integer","title":"Amount","default":0},"extra":{"additionalProperties":true,"type":"object","title":"Extra","default":{}}},"type":"object","required":["bank_account_id"],"title":"Account"},"BankProfileModel":{"properties":{"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"middle_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Middle Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"bank_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bank Name"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"balance":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Balance","default":"0"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","title":"BankProfileModel"},"BankProfileResponse":{"properties":{"bank_profile_external_id":{"type":"integer","title":"Bank Profile External Id"},"first_name":{"type":"string","title":"First Name"},"middle_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Middle Name"},"last_name":{"type":"string","title":"Last Name"},"bank_name":{"type":"string","title":"Bank Name"},"phone_number":{"type":"string","title":"Phone Number"},"balance":{"type":"string","title":"Balance"},"state":{"type":"string","title":"State"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"profile_id":{"type":"string","format":"uuid","title":"Profile Id"},"materials":{"anyOf":[{"items":{"$ref":"#/components/schemas/MaterialResponseModel"},"type":"array"},{"type":"null"}],"title":"Materials"},"currency":{"anyOf":[{"$ref":"#/components/schemas/CurrencyEnum"},{"type":"null"}]},"id":{"type":"string","format":"uuid","title":"Id"}},"type":"object","required":["bank_profile_external_id","first_name","last_name","bank_name","phone_number","balance","state","created_at","updated_at","profile_id","id"],"title":"BankProfileResponse"},"BaseLogResponse":{"properties":{},"type":"object","title":"BaseLogResponse"},"BaseResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse"},"BaseResponse_BankProfileModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/BankProfileModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[BankProfileModel]"},"BaseResponse_BankProfileResponse_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/BankProfileResponse"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[BankProfileResponse]"},"BaseResponse_BaseLogResponse_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/BaseLogResponse"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[BaseLogResponse]"},"BaseResponse_DesktopResponseModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/DesktopResponseModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[DesktopResponseModel]"},"BaseResponse_DeviceResponseModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/DeviceResponseModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[DeviceResponseModel]"},"BaseResponse_GetDeviceScreenshotModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/GetDeviceScreenshotModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[GetDeviceScreenshotModel]"},"BaseResponse_LoginResponseData_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/LoginResponseData"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[LoginResponseData]"},"BaseResponse_MaterialModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/MaterialModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[MaterialModel]"},"BaseResponse_MaterialResponseModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/MaterialResponseModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[MaterialResponseModel]"},"BaseResponse_NoneType_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"type":"null","title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[NoneType]"},"BaseResponse_ProfileResponse_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/ProfileResponse"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[ProfileResponse]"},"BaseResponse_list_Annotated_Union_CreateTransactionTask__FetchTransactionsTask__FetchProfileTask___FieldInfo_annotation_NoneType__required_True__discriminator__type_____":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/CreateTransactionTask-Output"},{"$ref":"#/components/schemas/FetchTransactionsTask"},{"$ref":"#/components/schemas/FetchProfileTask"}],"discriminator":{"propertyName":"type","mapping":{"CREATE_TRANSACTION":"#/components/schemas/CreateTransactionTask-Output","FETCH_PROFILE":"#/components/schemas/FetchProfileTask","FETCH_TRANSACTIONS":"#/components/schemas/FetchTransactionsTask"}}},"type":"array"},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[list[Annotated[Union[CreateTransactionTask, FetchTransactionsTask, FetchProfileTask], FieldInfo(annotation=NoneType, required=True, discriminator='type')]]]"},"BaseResponse_list_BankProfileResponse__":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"items":{"$ref":"#/components/schemas/BankProfileResponse"},"type":"array"},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[list[BankProfileResponse]]"},"BaseResponse_list_DesktopResponseModel__":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"items":{"$ref":"#/components/schemas/DesktopResponseModel"},"type":"array"},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[list[DesktopResponseModel]]"},"BaseResponse_list_DeviceResponseModel__":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"items":{"$ref":"#/components/schemas/DeviceResponseModel"},"type":"array"},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[list[DeviceResponseModel]]"},"BaseResponse_list_MaterialResponseModel__":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"items":{"$ref":"#/components/schemas/MaterialResponseModel"},"type":"array"},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[list[MaterialResponseModel]]"},"Body_add_screenshot_api_v1_device_add_screenshot__device_id__post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_add_screenshot_api_v1_device_add_screenshot__device_id__post"},"Body_log_monitoring_log_post":{"properties":{"type":{"$ref":"#/components/schemas/LogType"},"event":{"type":"string","title":"Event"},"message":{"type":"string","title":"Message"},"image":{"anyOf":[{"type":"string","format":"binary"},{"type":"string"},{"type":"null"}],"title":"Image"}},"type":"object","required":["type","event","message"],"title":"Body_log_monitoring_log_post"},"CreateBankProfile":{"properties":{"first_name":{"type":"string","title":"First Name"},"middle_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Middle Name"},"last_name":{"type":"string","title":"Last Name"},"bank_name":{"type":"string","title":"Bank Name"},"phone_number":{"type":"string","title":"Phone Number"},"balance":{"type":"string","title":"Balance"},"state":{"type":"string","title":"State"},"currency":{"anyOf":[{"$ref":"#/components/schemas/CurrencyEnum"},{"type":"null"}]}},"type":"object","required":["first_name","last_name","bank_name","phone_number","balance","state","currency"],"title":"CreateBankProfile"},"CreateDesktopData":{"properties":{"blue_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blue Token"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"contact":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact"},"timestamp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Timestamp"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"session_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Session Time"},"app_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Version"}},"type":"object","required":["blue_token","name","contact","app_version"],"title":"CreateDesktopData"},"CreateDeviceData":{"properties":{"desktop_id":{"type":"string","format":"uuid","title":"Desktop Id"},"status":{"$ref":"#/components/schemas/DeviceStatus"},"name":{"type":"string","title":"Name"},"timestamp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Timestamp"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"screen_width":{"type":"integer","title":"Screen Width"},"screen_height":{"type":"integer","title":"Screen Height"},"battery":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery"}},"type":"object","required":["desktop_id","status","name","screen_width","screen_height","battery"],"title":"CreateDeviceData"},"CreateMaterialRequest":{"properties":{"bank_profile_id":{"type":"string","format":"uuid","title":"Bank Profile Id"},"card_number":{"type":"string","title":"Card Number"},"name":{"type":"string","title":"Name"},"state":{"type":"string","title":"State"}},"type":"object","required":["bank_profile_id","card_number","name","state"],"title":"CreateMaterialRequest"},"CreateTransactionBody":{"properties":{"amount":{"type":"integer","title":"Amount"},"currency":{"$ref":"#/components/schemas/CurrencyEnum"},"type":{"$ref":"#/components/schemas/TransactionTypeEnum"},"bank_participator_id":{"type":"string","title":"Bank Participator Id"},"extra":{"additionalProperties":true,"type":"object","title":"Extra"}},"type":"object","required":["amount","currency","type","bank_participator_id"],"title":"CreateTransactionBody"},"CreateTransactionEvent":{"properties":{"type":{"type":"string","const":"CREATE_TRANSACTION","title":"Type"},"material_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"integer"},{"type":"string"}],"title":"Material Id"},"bank_name":{"type":"string","title":"Bank Name"},"data":{"$ref":"#/components/schemas/Transaction"}},"type":"object","required":["type","material_id","bank_name","data"],"title":"CreateTransactionEvent"},"CreateTransactionTask-Input":{"properties":{"bank_name":{"type":"string","title":"Bank Name"},"material_id":{"anyOf":[{"type":"integer"},{"type":"string","format":"uuid"}],"title":"Material Id"},"type":{"type":"string","const":"CREATE_TRANSACTION","title":"Type"},"body":{"$ref":"#/components/schemas/CreateTransactionBody"}},"type":"object","required":["bank_name","material_id","type","body"],"title":"CreateTransactionTask"},"CreateTransactionTask-Output":{"properties":{"bank_name":{"type":"string","title":"Bank Name"},"material_id":{"anyOf":[{"type":"integer"},{"type":"string","format":"uuid"}],"title":"Material Id"},"type":{"type":"string","const":"CREATE_TRANSACTION","title":"Type"},"body":{"$ref":"#/components/schemas/CreateTransactionBody"}},"type":"object","required":["bank_name","material_id","type","body"],"title":"CreateTransactionTask"},"CurrencyEnum":{"type":"integer","enum":[784,971,8,51,532,973,32,36,533,944,977,52,50,975,48,108,60,96,68,984,986,44,64,72,933,84,124,976,947,756,948,990,152,156,170,970,188,931,192,132,203,262,208,214,12,818,232,230,978,242,238,826,981,936,292,270,324,320,328,344,340,191,332,348,360,376,356,368,364,352,388,400,392,404,417,116,174,408,410,414,136,398,418,422,144,430,426,440,434,504,498,969,807,104,496,446,929,480,462,454,484,979,458,943,516,566,558,578,524,554,512,590,604,598,608,586,985,600,634,946,941,643,646,682,90,690,938,752,702,654,694,706,968,728,930,222,760,748,764,972,934,788,776,949,780,901,834,980,800,840,997,998,940,858,860,937,704,548,882,950,961,959,955,956,957,958,951,960,952,964,953,962,994,963,965,999,886,710,967,932],"title":"CurrencyEnum"},"DesktopResponseModel":{"properties":{"blue_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blue Token"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"contact":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact"},"timestamp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Timestamp"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"session_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Session Time"},"app_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Version"},"hidden":{"type":"boolean","title":"Hidden","default":false},"profile_id":{"type":"string","format":"uuid","title":"Profile Id"},"id":{"type":"string","format":"uuid","title":"Id"},"unique_name":{"type":"string","title":"Unique Name"},"devices":{"anyOf":[{"items":{"$ref":"#/components/schemas/DeviceResponseModel"},"type":"array"},{"type":"null"}],"title":"Devices","default":[]}},"type":"object","required":["blue_token","name","contact","timestamp","update_time","session_time","app_version","profile_id","id","unique_name"],"title":"DesktopResponseModel"},"DeviceResponseModel":{"properties":{"desktop_id":{"type":"string","format":"uuid","title":"Desktop Id"},"status":{"$ref":"#/components/schemas/DeviceStatus"},"name":{"type":"string","title":"Name"},"timestamp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Timestamp"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"screen_width":{"type":"integer","title":"Screen Width"},"screen_height":{"type":"integer","title":"Screen Height"},"battery":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery"},"id":{"type":"string","format":"uuid","title":"Id"},"unique_name":{"type":"string","title":"Unique Name"}},"type":"object","required":["desktop_id","status","name","timestamp","update_time","screen_width","screen_height","battery","id","unique_name"],"title":"DeviceResponseModel"},"DeviceStatus":{"type":"string","enum":["ONLINE","OFFLINE"],"title":"DeviceStatus"},"EmptyBody":{"properties":{},"type":"object","title":"EmptyBody"},"FetchProfileEvent":{"properties":{"type":{"type":"string","const":"FETCH_PROFILE","title":"Type"},"material_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"integer"},{"type":"string"}],"title":"Material Id"},"bank_name":{"type":"string","title":"Bank Name"},"data":{"$ref":"#/components/schemas/Account"}},"type":"object","required":["type","material_id","bank_name","data"],"title":"FetchProfileEvent"},"FetchProfileTask":{"properties":{"bank_name":{"type":"string","title":"Bank Name"},"material_id":{"anyOf":[{"type":"integer"},{"type":"string","format":"uuid"}],"title":"Material Id"},"type":{"type":"string","const":"FETCH_PROFILE","title":"Type"},"body":{"$ref":"#/components/schemas/EmptyBody"}},"type":"object","required":["bank_name","material_id","type","body"],"title":"FetchProfileTask"},"FetchTransactionsEvent":{"properties":{"type":{"type":"string","const":"FETCH_TRANSACTIONS","title":"Type"},"material_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"integer"},{"type":"string"}],"title":"Material Id"},"bank_name":{"type":"string","title":"Bank Name"},"data":{"items":{"$ref":"#/components/schemas/Transaction"},"type":"array","title":"Data"}},"type":"object","required":["type","material_id","bank_name","data"],"title":"FetchTransactionsEvent"},"FetchTransactionsRequest":{"properties":{"amount":{"type":"integer","title":"Amount","description":"Transaction amount in kopecks"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["amount","created_at"],"title":"FetchTransactionsRequest"},"FetchTransactionsTask":{"properties":{"bank_name":{"type":"string","title":"Bank Name"},"material_id":{"anyOf":[{"type":"integer"},{"type":"string","format":"uuid"}],"title":"Material Id"},"type":{"type":"string","const":"FETCH_TRANSACTIONS","title":"Type"},"body":{"anyOf":[{"$ref":"#/components/schemas/FetchTransactionsRequest"},{"$ref":"#/components/schemas/EmptyBody"}],"title":"Body"}},"type":"object","required":["bank_name","material_id","type"],"title":"FetchTransactionsTask"},"GetDeviceScreenshotModel":{"properties":{"device_id":{"type":"string","format":"uuid","title":"Device Id"},"screenshot_url":{"type":"string","title":"Screenshot Url"},"taken_at":{"type":"string","format":"date-time","title":"Taken At"},"id":{"type":"string","format":"uuid","title":"Id"}},"type":"object","required":["device_id","screenshot_url","taken_at","id"],"title":"GetDeviceScreenshotModel"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LogType":{"type":"string","enum":["INFO","WARNING","DEBUG","ERROR"],"title":"LogType"},"LoginRequestData":{"properties":{"api_key":{"type":"string","title":"Api Key"}},"type":"object","required":["api_key"],"title":"LoginRequestData"},"LoginResponseData":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"access_token":{"type":"string","title":"Access Token"},"refresh_token":{"type":"string","title":"Refresh Token"},"expires_in":{"type":"number","title":"Expires In"}},"type":"object","required":["id","access_token","refresh_token","expires_in"],"title":"LoginResponseData"},"MaterialModel":{"properties":{"bank_profile_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Bank Profile Id"},"card_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Card Number"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},"type":"object","title":"MaterialModel"},"MaterialResponseModel":{"properties":{"external_bank_profile_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"External Bank Profile Id"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"external_material_id":{"type":"integer","title":"External Material Id"},"card_number":{"type":"string","title":"Card Number"},"name":{"type":"string","title":"Name"},"bank_profile_id":{"type":"string","format":"uuid","title":"Bank Profile Id"},"id":{"type":"string","format":"uuid","title":"Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["external_bank_profile_id","state","external_material_id","card_number","name","bank_profile_id","id","created_at","updated_at"],"title":"MaterialResponseModel"},"ProfileResponse":{"properties":{"api_key":{"type":"string","title":"Api Key"},"id":{"type":"string","format":"uuid","title":"Id"},"bank_profiles":{"anyOf":[{"items":{"$ref":"#/components/schemas/BankProfileResponse"},"type":"array"},{"type":"null"}],"title":"Bank Profiles"}},"type":"object","required":["api_key","id","bank_profiles"],"title":"ProfileResponse"},"RefreshTokenData":{"properties":{"refresh_token":{"type":"string","title":"Refresh Token"}},"type":"object","required":["refresh_token"],"title":"RefreshTokenData"},"Transaction":{"properties":{"bank_transaction_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bank Transaction Id"},"amount":{"type":"integer","title":"Amount"},"currency":{"$ref":"#/components/schemas/CurrencyEnum"},"transaction_type":{"$ref":"#/components/schemas/TransactionTypeEnum"},"bank_participator_id":{"type":"string","title":"Bank Participator Id"},"status":{"anyOf":[{"$ref":"#/components/schemas/TransactionStatusEnum"},{"type":"null"}]},"extra":{"additionalProperties":true,"type":"object","title":"Extra","default":{}},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["amount","currency","transaction_type","bank_participator_id"],"title":"Transaction"},"TransactionStatusEnum":{"type":"string","enum":["progress","completed","declined"],"title":"TransactionStatusEnum"},"TransactionTypeEnum":{"type":"string","enum":["bank","top-up"],"title":"TransactionTypeEnum"},"UpdateBankProfileModel":{"properties":{"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"middle_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Middle Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"bank_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bank Name"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"balance":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Balance"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},"type":"object","title":"UpdateBankProfileModel"},"UpdateDesktopData":{"properties":{"blue_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blue Token"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"session_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Session Time"},"app_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Version"}},"type":"object","title":"UpdateDesktopData"},"UpdateDeviceData":{"properties":{"status":{"$ref":"#/components/schemas/DeviceStatus"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"battery":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery"}},"type":"object","required":["status"],"title":"UpdateDeviceData"},"UpdateMaterialData":{"properties":{"card_number":{"type":"string","title":"Card Number"}},"type":"object","required":["card_number"],"title":"UpdateMaterialData"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}} \ No newline at end of file diff --git a/main.cpp b/main.cpp index a93c060..bf1e2c3 100644 --- a/main.cpp +++ b/main.cpp @@ -1,7 +1,7 @@ #include #include -#include "dao/AccountDAO.h" +#include "dao/MaterialDAO.h" #include "views/MainWindow.h" #include "database/DatabaseManager.h" @@ -27,6 +27,7 @@ #include "black/PayByCardScript.h" #include "ozon/PayByPhoneScript.h" #include "widget/loader/ErrorWindow.h" +#include "widget/loader/LoaderDialog.h" #include "widget/loader/LoaderWindow.h" #include "widget/loader/RegistrationWindow.h" #include @@ -79,12 +80,12 @@ void startEventHandler(const QCoreApplication &app) { void startPayByPhoneTest() { // Берём первый девайс с ozon аккаунтом - QList accounts = AccountDAO::getAccountsByAppCode("ozon"); + QList accounts = MaterialDAO::getAccountsByAppCode("ozon"); if (accounts.isEmpty()) { qWarning() << "[PayByPhoneTest] No ozon accounts found"; return; } - AccountInfo account = accounts.first(); + MaterialInfo account = accounts.first(); qDebug() << "[PayByPhoneTest] Using account:" << account.id << account.deviceId << account.lastNumbers; auto *thread = new QThread(nullptr); @@ -107,12 +108,12 @@ void startPayByPhoneTest() { } void startBlackPayByCardTest() { - QList accounts = AccountDAO::getAccountsByAppCode("black"); + QList accounts = MaterialDAO::getAccountsByAppCode("black"); if (accounts.isEmpty()) { qWarning() << "[BlackPayByCardTest] No black accounts found"; return; } - AccountInfo account = accounts.first(); + MaterialInfo account = accounts.first(); qDebug() << "[BlackPayByCardTest] Using account:" << account.id << account.deviceId << account.lastNumbers; auto *thread = new QThread(nullptr); @@ -143,7 +144,22 @@ void startNetworkService(const QCoreApplication &app) { QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater); QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() { + auto *loader = new LoaderDialog(); + loader->setWindowTitle("Завершение..."); + loader->show(); + QCoreApplication::processEvents(); + + QEventLoop loop; + QObject::connect(networkService, &NetworkService::finished, &loop, &QEventLoop::quit); + + // Таймаут на случай если сервер недоступен + QTimer::singleShot(10000, &loop, &QEventLoop::quit); + networkService->stop(); + loop.exec(); + + loader->close(); + delete loader; }); // Сигнал эмитируется из фонового потока → Qt::QueuedConnection доставит его в главный поток diff --git a/migrations.sql b/migrations.sql index c649137..fba728f 100644 --- a/migrations.sql +++ b/migrations.sql @@ -1,11 +1,12 @@ -- ============================================================ --- Версия 1 (user_version = 1): начальная схема +-- Схема БД ARCDeskProject -- ============================================================ CREATE TABLE IF NOT EXISTS devices ( id TEXT PRIMARY KEY, api_id TEXT, + adb_serial TEXT, status TEXT, name TEXT, android TEXT, @@ -17,7 +18,7 @@ CREATE TABLE IF NOT EXISTS devices image BLOB ); -CREATE TABLE IF NOT EXISTS applications +CREATE TABLE IF NOT EXISTS bank_profiles ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT, @@ -29,12 +30,18 @@ CREATE TABLE IF NOT EXISTS applications status TEXT DEFAULT 'off', install TEXT DEFAULT 'no', comment TEXT, + phone TEXT DEFAULT '', + email TEXT DEFAULT '', + full_name TEXT DEFAULT '', + bank_profile_id TEXT DEFAULT '', + currency TEXT DEFAULT '', + amount REAL DEFAULT 0, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, UNIQUE (device_id, code) ); -- app_code: код банка ("ozon", "black") — не путать с человекочитаемым name -CREATE TABLE IF NOT EXISTS accounts +CREATE TABLE IF NOT EXISTS materials ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT, @@ -45,6 +52,8 @@ CREATE TABLE IF NOT EXISTS accounts currency TEXT DEFAULT '', update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), status TEXT DEFAULT 'active', + card_number TEXT DEFAULT '', + material_id TEXT DEFAULT '', FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, UNIQUE (device_id, app_code, last_numbers) ); @@ -70,14 +79,15 @@ CREATE TABLE IF NOT EXISTS transactions update_time DATETIME, complete_time DATETIME, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE + receipt_image BLOB, + FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER, - external_id INTEGER, + external_id TEXT, device_id TEXT, amount REAL, phone TEXT, @@ -88,15 +98,46 @@ CREATE TABLE IF NOT EXISTS events 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) + json TEXT DEFAULT '', + sent_to_server INTEGER DEFAULT 0, + screenshot BLOB, + FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE, + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE ); --- ============================================================ --- Версия 2 (user_version = 2): currency + rename app_name→app_code --- ============================================================ +CREATE TABLE IF NOT EXISTS banks +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alias TEXT UNIQUE, + name TEXT, + nspk_name TEXT +); --- Для существующих БД (применяется автоматически при старте): --- ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT ''; --- ALTER TABLE accounts RENAME COLUMN app_name TO app_code; +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 +); + +CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC); + +CREATE TABLE IF NOT EXISTS general_logs +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '', + message TEXT NOT NULL, + timestamp TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_general_logs_id ON general_logs (id DESC); + +CREATE TABLE IF NOT EXISTS settings +( + key TEXT PRIMARY KEY, + value TEXT +); diff --git a/models/db/AccountInfo.h b/models/db/AccountInfo.h deleted file mode 100644 index 216a5e3..0000000 --- a/models/db/AccountInfo.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include - -enum class AccountStatus { - Active, - Off -}; - -inline QString accountStatusToString(const AccountStatus status) { - switch (status) { - case AccountStatus::Active: return "active"; - case AccountStatus::Off: return "off"; - default: return "active"; - } -} - -inline QString accountStatusToUiString(const AccountStatus status) { - switch (status) { - case AccountStatus::Active: return "в работе"; - case AccountStatus::Off: return "выключена"; - default: return "в работе"; - } -} - -inline AccountStatus accountStatusFromString(const QString &str) { - if (str == "active") return AccountStatus::Active; - if (str == "off") return AccountStatus::Off; - return AccountStatus::Active; -} - -struct AccountInfo { - int id = -1; - QString deviceId; // code - QString appCode; - QString lastNumbers; - QString cardNumber; // full 16-digit card number - QString materialId; - QString currency; - QString description; - double amount = 0.0; - QDateTime updateTime; - AccountStatus status = AccountStatus::Active; -}; \ No newline at end of file diff --git a/models/db/ApplicationInfo.h b/models/db/BankProfileInfo.h similarity index 94% rename from models/db/ApplicationInfo.h rename to models/db/BankProfileInfo.h index ff2f103..0304f5b 100644 --- a/models/db/ApplicationInfo.h +++ b/models/db/BankProfileInfo.h @@ -4,7 +4,7 @@ #include #include -struct ApplicationInfo { +struct BankProfileInfo { int id = -1; bool install = false; bool pinCodeChecked = false; diff --git a/models/db/DeviceInfo.h b/models/db/DeviceInfo.h index 3af8a7b..3e4e810 100644 --- a/models/db/DeviceInfo.h +++ b/models/db/DeviceInfo.h @@ -3,8 +3,9 @@ #include struct DeviceInfo { - QString id; // ADB device id (e.g. "emulator-5554") - QString apiId; // UUID from backend API (DeviceResponseModel.id) + QString id; // android_id (unique device identifier) + QString apiId; // UUID from backend API (DeviceResponseModel.id) + QString adbSerial; // ADB serial (e.g. "emulator-5554", "7dfc444d43fbeab7") QString status; QString name; QString android; diff --git a/models/db/MaterialInfo.h b/models/db/MaterialInfo.h new file mode 100644 index 0000000..b54020a --- /dev/null +++ b/models/db/MaterialInfo.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +enum class MaterialStatus { + Active, + Off +}; + +inline QString materialStatusToString(const MaterialStatus status) { + switch (status) { + case MaterialStatus::Active: return "active"; + case MaterialStatus::Off: return "off"; + default: return "active"; + } +} + +inline QString materialStatusToUiString(const MaterialStatus status) { + switch (status) { + case MaterialStatus::Active: return "в работе"; + case MaterialStatus::Off: return "выключена"; + default: return "в работе"; + } +} + +inline MaterialStatus materialStatusFromString(const QString &str) { + if (str == "active") return MaterialStatus::Active; + if (str == "off") return MaterialStatus::Off; + return MaterialStatus::Active; +} + +struct MaterialInfo { + int id = -1; + QString deviceId; // code + QString appCode; + QString lastNumbers; + QString cardNumber; // full 16-digit card number + QString materialId; + QString currency; + QString description; + double amount = 0.0; + QDateTime updateTime; + MaterialStatus status = MaterialStatus::Active; +}; \ No newline at end of file diff --git a/services/DeviceScreener.cpp b/services/DeviceScreener.cpp index 4850d89..e2af28c 100644 --- a/services/DeviceScreener.cpp +++ b/services/DeviceScreener.cpp @@ -19,7 +19,8 @@ #include #include "adb/AdbUtils.h" -#include "dao/ApplicationDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/MaterialDAO.h" #include "dao/SettingsDAO.h" DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) { @@ -181,12 +182,13 @@ QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) { } QJsonObject body; - body["desktop_id"] = desktopId; - body["status"] = "ONLINE"; - body["name"] = device.name; - body["screen_width"] = device.screenWidth; - body["screen_height"] = device.screenHeight; - body["battery"] = device.battery; + body["desktop_id"] = desktopId; + body["bank_profile_id"] = QJsonValue::Null; + body["status"] = "ONLINE"; + body["name"] = device.name; + body["screen_width"] = device.screenWidth; + body["screen_height"] = device.screenHeight; + body["battery"] = device.battery; const QJsonValue result = apiPost("/api/v1/device/", body); if (!result.isNull() && !result.isUndefined()) { @@ -239,25 +241,42 @@ void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray } void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) { + // Находим первый активный bank_profile для этого устройства + QJsonValue bankProfileId = QJsonValue::Null; + const QList apps = BankProfileDAO::getAllApplicationsByDeviceId(device.id); + for (const BankProfileInfo &app : apps) { + if (!app.bankProfileId.isEmpty() && app.status == "active") { + bankProfileId = app.bankProfileId; + break; + } + } + QJsonObject body; - body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE"; - body["battery"] = device.battery; - body["update_time"] = QDateTime::currentSecsSinceEpoch(); + body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE"; + body["bank_profile_id"] = bankProfileId; + body["battery"] = device.battery; + body["update_time"] = QDateTime::currentSecsSinceEpoch(); apiPatch("/api/v1/device/" + device.apiId, body); } void DeviceScreener::toggleBankProfilesOnApi(const QString &deviceId, bool enable) { - const QList apps = ApplicationDAO::getApplicationsByDeviceId(deviceId); - for (const ApplicationInfo &app : apps) { + if (enable) return; // не включаем автоматически при появлении устройства + + const QList apps = BankProfileDAO::getAllApplicationsByDeviceId(deviceId); + for (const BankProfileInfo &app : apps) { if (app.bankProfileId.isEmpty()) continue; + if (app.status != "active") continue; - // При включении — только если приложение активно в системе - if (enable && app.status != "active") continue; + apiPatch("/api/v1/profile/bank_profile/" + app.bankProfileId + "/false", QJsonObject()); - const QString enableStr = enable ? "true" : "false"; - const QString path = "/api/v1/profile/bank_profile/" + app.bankProfileId + "/" + enableStr; - apiPatch(path, QJsonObject()); + const QList materials = MaterialDAO::getAccounts(deviceId, app.code); + for (const MaterialInfo &mat : materials) { + if (mat.materialId.isEmpty()) continue; + apiPatch("/api/v1/profile/material/" + mat.materialId + "/false", QJsonObject()); + } } + + BankProfileDAO::deactivateAppsForDevice(deviceId); } QJsonValue DeviceScreener::apiGet(const QString &path) { @@ -304,24 +323,39 @@ void DeviceScreener::start() { QList > devices = readAdbDevices(); QStringList onlineIds; QList deviceInfos; - for (const auto &[id, status]: devices) { + for (const auto &[adbSerial, status]: devices) { DeviceInfo device; - device.id = id; + bool isChecked = true; if (status == "device") { + device = readDeviceInfo(adbSerial); + + // Проверяем: есть ли уже устройство с таким id в БД (с apiId) + const DeviceInfo existingById = DeviceDAO::getDeviceById(device.id); + if (!existingById.id.isEmpty() && !existingById.apiId.isEmpty()) { + device.apiId = existingById.apiId; + } else { + // Маппинг с восстановленным устройством — только по name (точное совпадение) + const DeviceInfo restored = DeviceDAO::findByName(device.name); + if (!restored.id.isEmpty() && restored.id != device.id && !restored.apiId.isEmpty()) { + device.apiId = restored.apiId; + DeviceDAO::deleteDevice(restored.id); + qDebug() << "[DeviceScreener] matched restored device:" << restored.id << "->" << device.id + << "apiId:" << device.apiId; + } + } + onlineIds << device.id; - const bool isChecked = prevOnline.contains(id); - device = readDeviceInfo(id, isChecked); - device.image = takeScreenshot(id); + isChecked = prevOnline.contains(device.id); + + device.image = takeScreenshot(adbSerial); if (!device.image.isEmpty()) { - if (!DeviceDAO::updateImage(id, device.image)) { + if (!DeviceDAO::updateImage(device.id, device.image)) { qDebug() << "Cant update screenshot"; } } - if (!isChecked) { - // Новое устройство — включаем active профили - toggleBankProfilesOnApi(id, true); - } + } else { + device.id = adbSerial; } device.status = status; device.updateTime = QDateTime::currentDateTime(); @@ -330,6 +364,16 @@ void DeviceScreener::start() { if (!DeviceDAO::upsertDevice(device)) { qDebug() << "Cant update device"; } + + if (!isChecked) { + // Новое устройство — проверяем приложения (после upsert, чтобы FK не падал) + QSet apps = AdbUtils::getListApps(adbSerial); + if (!BankProfileDAO::checkInstalledApps(device.id, apps)) { + qWarning() << "Cant check installed apps"; + } else { + qDebug() << "Apps checked:" << device.id; + } + } } // Синхронизация с API @@ -361,10 +405,17 @@ void DeviceScreener::start() { qDebug() << "Cant mark devices offline"; } - // Было онлайн в БД, сейчас офлайн — выключаем профили на сервере + // Было онлайн в БД, сейчас офлайн — выключаем профили и обновляем статус на сервере for (const QString &id : prevOnline) { if (!onlineIds.contains(id)) { toggleBankProfilesOnApi(id, false); + // Отправляем OFFLINE статус на сервер + const DeviceInfo offlineDevice = DeviceDAO::getDeviceById(id); + if (!offlineDevice.apiId.isEmpty()) { + DeviceInfo offlineCopy = offlineDevice; + offlineCopy.status = "offline"; + updateDeviceOnApi(offlineCopy); + } } } @@ -452,9 +503,8 @@ QList > DeviceScreener::readAdbDevices() { static const QRegularExpression screenRegex(R"(Physical size: (\d+)x(\d+))"); static const QRegularExpression batteryRegex(R"(level:\s*(\d+))"); -DeviceInfo DeviceScreener::readDeviceInfo(const QString &deviceId, const bool isChecked) { +DeviceInfo DeviceScreener::readDeviceInfo(const QString &adbSerial) { DeviceInfo info; - info.id = deviceId; info.screenWidth = 0; info.screenHeight = 0; info.battery = 0; @@ -462,12 +512,17 @@ DeviceInfo DeviceScreener::readDeviceInfo(const QString &deviceId, const bool is auto runAdbCommand = [&](const QStringList &args) -> QString { QProcess process; process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath()); - process.start(AdbUtils::adbPath(), QStringList{"-s", deviceId} + args); + process.start(AdbUtils::adbPath(), QStringList{"-s", adbSerial} + args); process.waitForFinished(); return QString::fromUtf8(process.readAllStandardOutput()).trimmed(); }; - info.name = runAdbCommand({"shell", "getprop", "ro.product.model"}); + const QString androidId = runAdbCommand({"shell", "settings", "get", "secure", "android_id"}); + const QString model = runAdbCommand({"shell", "getprop", "ro.product.model"}); + + info.id = androidId.isEmpty() ? adbSerial : androidId; + info.adbSerial = adbSerial; + info.name = model + "-" + info.id; info.android = runAdbCommand({"shell", "getprop", "ro.build.version.release"}); // Размер экрана @@ -485,15 +540,6 @@ DeviceInfo DeviceScreener::readDeviceInfo(const QString &deviceId, const bool is info.battery = match.captured(1).toInt(); } - if (!isChecked) { - QSet apps = AdbUtils::getListApps(deviceId); - if (!ApplicationDAO::checkInstalledApps(deviceId, apps)) { - qWarning() << "Cant check installed apps"; - } else { - qDebug() << "Apps checked: " + deviceId; - } - } - info.updateTime = QDateTime::currentDateTime(); return info; } diff --git a/services/DeviceScreener.h b/services/DeviceScreener.h index 6a23248..ef5ca93 100644 --- a/services/DeviceScreener.h +++ b/services/DeviceScreener.h @@ -33,7 +33,7 @@ private: static QByteArray takeScreenshot(const QString &deviceId); - static DeviceInfo readDeviceInfo(const QString &deviceId, bool isChecked); + static DeviceInfo readDeviceInfo(const QString &adbSerial); void postDevicesData(const QList &devices); diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index 8886b38..4c2ffb5 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -10,13 +10,13 @@ #include #include -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/BankDAO.h" #include "dao/DeviceDAO.h" #include "dao/EventDAO.h" #include "dao/TransactionDAO.h" -#include "db/ApplicationInfo.h" +#include "db/BankProfileInfo.h" #include "adb/AdbUtils.h" #include "net/NetworkService.h" #include "black/GetLastTransactionsScript.h" @@ -50,7 +50,7 @@ void EventHandler::start() { // Запрашиваем новые задачи только для устройств без pending events const QSet busyDevices = EventDAO::getDevicesWithPendingEvents(); - const QStringList materialIds = AccountDAO::getActiveMaterialIdsExcludingDevices(busyDevices); + const QStringList materialIds = MaterialDAO::getActiveMaterialIdsExcludingDevices(busyDevices); if (!materialIds.isEmpty()) { m_network->getTasks(materialIds); } @@ -99,13 +99,18 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) { } // Ищем аккаунт по material_id напрямую - const AccountInfo account = AccountDAO::findByMaterialId(materialId); + const MaterialInfo account = MaterialDAO::findByMaterialId(materialId); if (account.id == -1) { qWarning() << "[EventHandler] Account not found for bank:" << bankName << "material_id:" << materialId; sendTaskError(typeStr, materialId, bankName, "Account not found for material_id: " + materialId); continue; } + // FETCH_PROFILE: отменяем старые ожидающие задачи такого же типа на этом устройстве + if (eventType == EventType::FetchProfile) { + EventDAO::cancelWaitingByType(account.deviceId, EventType::FetchProfile); + } + EventInfo event; event.status = EventStatus::Wait; event.type = eventType; @@ -227,18 +232,44 @@ static void sendEventStatus(const QString &deviceId, const QString &externalId, } void EventHandler::updateEventThread(const QString &key, const EventInfo &event, const QString &result) { + // FETCH_TRANSACTIONS: если транзакция не найдена и прошло менее 10 минут — повторяем + if (!result.isEmpty() && event.type == EventType::FetchTransactions + && result.startsWith("Transaction not found")) { + const qint64 elapsedSecs = event.timestamp.secsTo(QDateTime::currentDateTimeUtc()); + if (elapsedSecs < 600) { + qDebug() << "[EventHandler] FETCH_TRANSACTIONS not found, requeueing (elapsed:" << elapsedSecs << "s)"; + EventDAO::updateEvent(event.id, EventStatus::Wait, result); + m_threads.remove(key); + // Повтор через 15 секунд + QTimer::singleShot(15000, this, [this, key]() { + processNextTask(key); + }); + return; + } + qWarning() << "[EventHandler] FETCH_TRANSACTIONS timeout (elapsed:" << elapsedSecs << "s), sending error"; + } + const EventStatus newStatus = result.isEmpty() ? EventStatus::Complete : EventStatus::Error; if (!EventDAO::updateEvent(event.id, newStatus, result)) { qCritical() << "[EventHandler] Failed to update event:" << event.id; } else { if (newStatus == EventStatus::Error) { + // Сохраняем скриншот устройства при ошибке + if (!event.deviceId.isEmpty()) { + const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId); + const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial; + const QByteArray screenshot = AdbUtils::captureScreenshotBytes(adbSerial); + if (!screenshot.isEmpty()) { + EventDAO::updateScreenshot(event.id, screenshot); + } + } sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result); EventDAO::markSentToServer(event.id); } if (newStatus != EventStatus::Error) { - const AccountInfo account = AccountDAO::getAccountById(event.accountId); + const MaterialInfo account = MaterialDAO::getAccountById(event.accountId); const QMap currencyCodes = { {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32} }; @@ -353,7 +384,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event, processNextTask(key); } -void EventHandler::startThreadForTask(const AccountInfo &account, const EventInfo &event) { +void EventHandler::startThreadForTask(const MaterialInfo &account, const EventInfo &event) { const auto thr = new QThread(this); const QString key = account.deviceId; @@ -373,17 +404,27 @@ void EventHandler::startThreadForTask(const AccountInfo &account, const EventInf }; const QString &appCode = account.appCode; - const QString &deviceId = account.deviceId; + // Скриптам нужен ADB serial для команд, не android_id + const DeviceInfo device = DeviceDAO::getDeviceById(account.deviceId); + const QString adbSerial = device.adbSerial.isEmpty() ? account.deviceId : device.adbSerial; if (event.type == EventType::FetchProfile) { - if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(deviceId, appCode)); - else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(deviceId, appCode)); + if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true)); + else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(adbSerial, appCode)); else qWarning() << "[EventHandler] FETCH_PROFILE: unknown appCode:" << appCode; } if (event.type == EventType::FetchTransactions) { - if (appCode == "ozon") setup(new Ozon::GetLastTransactionsScript(deviceId, appCode)); - else if (appCode == "black") setup(new Black::GetLastTransactionsScript(deviceId, appCode)); + // Парсим amount и created_at из body задачи + const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object(); + const QJsonObject body = taskJson["body"].toObject(); + const double searchAmount = body["amount"].toDouble() / 100.0; // копейки → рубли + const QDateTime searchTime = QDateTime::fromString(body["created_at"].toString(), Qt::ISODate); + + if (appCode == "ozon") + setup(new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime)); + else if (appCode == "black") + setup(new Black::GetLastTransactionsScript(adbSerial, appCode)); else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode; } @@ -448,8 +489,8 @@ void EventHandler::processNextTask(const QString &deviceId) { continue; } - const AccountInfo account = AccountDAO::getAccountById(event.accountId); - const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, account.appCode); + const MaterialInfo account = MaterialDAO::getAccountById(event.accountId); + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, account.appCode); if (app.status != "active") { qWarning() << "[EventHandler] Bank profile disabled for" << account.appCode << "device:" << deviceId; EventDAO::updateEvent(event.id, EventStatus::Error, "Bank profile is disabled"); diff --git a/services/EventHandler.h b/services/EventHandler.h index ad958a5..1c348d6 100644 --- a/services/EventHandler.h +++ b/services/EventHandler.h @@ -4,7 +4,7 @@ #include #include -#include "db/AccountInfo.h" +#include "db/MaterialInfo.h" #include "db/EventInfo.h" class NetworkService; @@ -39,7 +39,7 @@ private: void updateEventThread(const QString &key, const EventInfo &event, const QString &result); - void startThreadForTask(const AccountInfo &account, const EventInfo &event); + void startThreadForTask(const MaterialInfo &account, const EventInfo &event); void processNextTask(const QString &deviceId); }; diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index f19dfa5..edfb094 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -11,8 +11,9 @@ #include #include -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/BankDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/EventDAO.h" #include "dao/SettingsDAO.h" @@ -181,7 +182,7 @@ void NetworkService::getPayments() { QString deviceId = obj["deviceId"].toString(); QString appName = obj["appName"].toString(); QString lastNumbers = obj["lastNumbers"].toString(); - AccountInfo account = AccountDAO::findAppAccount(deviceId, appName, lastNumbers); + MaterialInfo account = MaterialDAO::findAppAccount(deviceId, appName, lastNumbers); eventInfo.accountId = account.id; eventInfo.deviceId = account.deviceId; @@ -291,10 +292,37 @@ void NetworkService::loginAndSetup() { const qint64 expiresIn = static_cast(loginData["expires_in"].toDouble()); SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); - // Шаг 2: проверяем есть ли desktop_id - const QString desktopId = SettingsDAO::get("desktop_id"); + // Шаг 2: получаем актуальный desktop с сервера + QString desktopId = SettingsDAO::get("desktop_id"); + + // Всегда проверяем список десктопов — desktop_id мог устареть или быть удалён + const QJsonValue existingDesktops = getJson(m_apiBase + m_pathDesktopCreate, true); + if (!existingDesktops.isUndefined() && !existingDesktops.isNull() && existingDesktops.isArray()) { + const QJsonArray desktopsArray = existingDesktops.toArray(); + QString foundId; + QString foundProfileId; + for (const QJsonValue &dVal : desktopsArray) { + const QJsonObject d = dVal.toObject(); + if (d["hidden"].toBool(false)) continue; + + foundId = d["id"].toString(); + foundProfileId = d["profile_id"].toString(); + if (!foundId.isEmpty()) break; + } + + if (!foundId.isEmpty() && foundId != desktopId) { + // Нашли актуальный десктоп, отличается от сохранённого — обновляем + qDebug() << "[loginAndSetup] desktop_id updated:" << desktopId << "->" << foundId; + desktopId = foundId; + SettingsDAO::set("desktop_id", desktopId); + SettingsDAO::set("profile_id", foundProfileId); + } else if (!foundId.isEmpty()) { + qDebug() << "[loginAndSetup] desktop_id confirmed:" << desktopId; + } + } + if (desktopId.isEmpty()) { - // Шаг 3: создаём desktop + // Десктопов на сервере нет — создаём новый QJsonObject desktopBody; desktopBody["blue_token"] = QJsonValue::Null; desktopBody["name"] = QJsonValue::Null; @@ -310,13 +338,38 @@ void NetworkService::loginAndSetup() { } const QJsonObject desktopData = desktopResult.toObject(); - SettingsDAO::set("desktop_id", desktopData["id"].toString()); + desktopId = desktopData["id"].toString(); + SettingsDAO::set("desktop_id", desktopId); SettingsDAO::set("profile_id", desktopData["profile_id"].toString()); } - // Шаг 4: синхронизация устройств и профилей + // Шаг 3: синхронизация устройств с сервера + syncDevicesFromServer(desktopId); + + // Шаг 4: синхронизация банковских профилей и материалов с сервера + syncBankProfilesFromServer(); + + // Шаг 5: загрузка справочника банков если пустой + syncBanksIfEmpty(); + + // Шаг 6: синхронизация статусов профилей/материалов с учётом онлайн-устройств syncCurrentProfile(); + // Шаг 7: отправляем OFFLINE статус всех устройств (DeviceScreener обновит на ONLINE при обнаружении) + { + const QList allDevices = DeviceDAO::getAllDevices(false); + for (const DeviceInfo &d : allDevices) { + if (d.apiId.isEmpty()) continue; + QJsonObject body; + body["status"] = "OFFLINE"; + body["bank_profile_id"] = QJsonValue::Null; + body["battery"] = d.battery; + body["update_time"] = QDateTime::currentSecsSinceEpoch(); + patchJson(m_apiBase + m_pathDevice + d.apiId, body, true); + qDebug() << "[loginAndSetup] sent OFFLINE for device" << d.id << d.apiId; + } + } + emit finishedWithResult(1); emit finished(); } @@ -412,6 +465,49 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod return result; } +QJsonValue NetworkService::deleteJson(const QString &path, bool withAuth) { + const QUrl url(path); + QNetworkRequest request(url); + if (withAuth && !m_accessToken.isEmpty()) { + request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + } + + QNetworkReply *reply = m_manager->deleteResource(request); + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); + + QJsonValue result; + if (!timeout.isActive()) { + qCritical() << "Таймаут запроса (DELETE):" << path; + reply->abort(); + result = QJsonValue::Undefined; + } else if (reply->error() != QNetworkReply::NoError) { + qCritical() << "Network error (DELETE):" << reply->errorString() << path; + result = QJsonValue::Undefined; + } else { + const QByteArray raw = reply->readAll(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error == QJsonParseError::NoError && doc.isObject()) { + const QJsonObject root = doc.object(); + if (root["success"].toBool()) { + result = root["data"]; + } else { + qCritical() << "API error (DELETE):" << root; + result = QJsonValue::Null; + } + } + } + reply->deleteLater(); + return result; +} + QJsonValue NetworkService::getJson(const QString &path, bool withAuth) { const QUrl url(path); QNetworkRequest request(url); @@ -457,6 +553,255 @@ QJsonValue NetworkService::getJson(const QString &path, bool withAuth) { return result; } +void NetworkService::syncBankProfilesFromServer() { + const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true); + if (profileResult.isUndefined() || profileResult.isNull()) { + qWarning() << "[syncBankProfilesFromServer] failed to get current_profile"; + return; + } + + const QJsonObject profileData = profileResult.toObject(); + const QJsonArray bankProfiles = profileData["bank_profiles"].toArray(); + + // Маппинг apiId → локальный device_id + const QList allDevices = DeviceDAO::getAllDevices(false); + QMap apiIdToLocalId; + for (const DeviceInfo &d : allDevices) { + if (!d.apiId.isEmpty()) { + apiIdToLocalId[d.apiId] = d.id; + } + } + + const QSettings configSettings("config.ini", QSettings::IniFormat); + + for (const QJsonValue &bpVal : bankProfiles) { + const QJsonObject bp = bpVal.toObject(); + const QString bpId = bp["id"].toString(); + const QString bankName = bp["bank_name"].toString(); + const QString bpState = bp["state"].toString(); + const QString phone = bp["phone_number"].toString(); + const QString firstName = bp["first_name"].toString(); + const QString middleName = bp["middle_name"].toString(); + const QString lastName = bp["last_name"].toString(); + const QString currency = bp["currency"].isNull() ? "" : QString::number(bp["currency"].toInt()); + const QString serverDeviceId = bp["device_id"].toString(); // API UUID устройства + + if (bpId.isEmpty() || bankName.isEmpty()) continue; + + // Определяем локальный device_id по API UUID + const QString localDeviceId = apiIdToLocalId.value(serverDeviceId); + if (localDeviceId.isEmpty()) { + qDebug() << "[syncBankProfilesFromServer] device not found locally for api_id" << serverDeviceId << ", skipping bp" << bpId; + continue; + } + + const QString fullName = (firstName + " " + (middleName.isEmpty() ? "" : middleName + " ") + lastName).trimmed(); + + BankProfileInfo app = BankProfileDAO::getApplication(localDeviceId, bankName); + if (app.id < 0) { + // Приложение не обнаружено на устройстве — создаём запись + BankProfileInfo newApp; + newApp.deviceId = localDeviceId; + newApp.code = bankName; + newApp.name = configSettings.value(bankName + "/name", bankName).toString(); + newApp.package = configSettings.value(bankName + "/android_package_name").toString(); + newApp.status = (bpState == "disabled") ? "off" : "active"; + newApp.install = false; + newApp.pinCodeChecked = false; + newApp.bankProfileId = bpId; + newApp.fullName = fullName; + newApp.phone = phone; + newApp.currency = currency.isEmpty() + ? configSettings.value(bankName + "/currency").toString() + : currency; + + if (BankProfileDAO::upsertApplication(newApp)) { + app = BankProfileDAO::getApplication(localDeviceId, bankName); + qDebug() << "[syncBankProfilesFromServer] created app" << app.id << bankName << "device" << localDeviceId; + } else { + qWarning() << "[syncBankProfilesFromServer] failed to create app" << bankName << "device" << localDeviceId; + continue; + } + } else { + // Application существует — привязываем bank_profile_id если нужно + if (app.bankProfileId != bpId) { + BankProfileDAO::updateBankProfileId(app.id, bpId); + qDebug() << "[syncBankProfilesFromServer] linked bp" << bpId << "to app" << app.id; + } + if (app.fullName.isEmpty() || app.phone.isEmpty()) { + BankProfileDAO::updateProfileInfo(app.id, fullName, phone); + } + } + + // Синхронизируем materials → accounts + const QJsonArray materials = bp["materials"].toArray(); + for (const QJsonValue &matVal : materials) { + const QJsonObject mat = matVal.toObject(); + const QString matId = mat["id"].toString(); + const QString cardNumber = mat["card_number"].toString(); + if (matId.isEmpty()) continue; + + // Уже есть account с этим material_id — пропускаем + const MaterialInfo existing = MaterialDAO::findByMaterialId(matId); + if (existing.id >= 0) continue; + + const QString lastNumbers = cardNumber.length() >= 4 ? cardNumber.right(4) : cardNumber; + + // Ищем существующий account по last_numbers + MaterialInfo acc = MaterialDAO::findAppAccount(localDeviceId, bankName, lastNumbers); + if (acc.id >= 0) { + if (acc.materialId.isEmpty()) { + MaterialDAO::updateMaterialId(acc.id, matId); + if (acc.cardNumber.isEmpty() && !cardNumber.isEmpty()) { + MaterialDAO::updateCardNumber(acc.id, cardNumber); + } + qDebug() << "[syncBankProfilesFromServer] linked material" << matId << "to account" << acc.id; + } + } else if (!lastNumbers.isEmpty()) { + // Account не существует — создаём + MaterialInfo newAcc; + newAcc.deviceId = localDeviceId; + newAcc.appCode = bankName; + newAcc.lastNumbers = lastNumbers; + newAcc.cardNumber = cardNumber; + newAcc.materialId = matId; + newAcc.currency = currency.isEmpty() + ? configSettings.value(bankName + "/currency").toString() + : currency; + newAcc.status = MaterialStatus::Active; + newAcc.updateTime = QDateTime::currentDateTime(); + + if (MaterialDAO::upsertAccount(newAcc)) { + qDebug() << "[syncBankProfilesFromServer] created account for material" << matId << "card" << lastNumbers; + } else { + qWarning() << "[syncBankProfilesFromServer] failed to create account for material" << matId; + } + } + } + } + + qDebug() << "[syncBankProfilesFromServer] done"; +} + +void NetworkService::syncBanksIfEmpty() { + const QList existing = BankDAO::getAll(); + if (!existing.isEmpty()) { + qDebug() << "[syncBanksIfEmpty] banks table has" << existing.size() << "entries, skipping"; + return; + } + + qDebug() << "[syncBanksIfEmpty] banks table empty, downloading..."; + + QNetworkRequest request(QUrl("https://api.brtservice.io/api/rest/info/")); + QNetworkReply *reply = m_manager->get(request); + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); + + if (!timeout.isActive()) { + qWarning() << "[syncBanksIfEmpty] timeout"; + reply->abort(); + reply->deleteLater(); + return; + } + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[syncBanksIfEmpty] error:" << reply->errorString(); + reply->deleteLater(); + return; + } + + const QByteArray data = reply->readAll(); + reply->deleteLater(); + + const QJsonDocument doc = QJsonDocument::fromJson(data); + QJsonArray array; + if (doc.isArray()) { + array = doc.array(); + } else if (doc.isObject() && doc.object().contains("data")) { + array = doc.object()["data"].toArray(); + } + + QList banks; + for (const QJsonValue &val : array) { + const QJsonObject obj = val.toObject(); + BankInfo bank; + bank.alias = obj["alias"].toString(); + bank.name = obj["name"].toString(); + bank.nspkName = obj["nspk_name"].toString(); + if (!bank.alias.isEmpty()) { + banks.append(bank); + } + } + + if (!banks.isEmpty()) { + BankDAO::replaceAll(banks); + qDebug() << "[syncBanksIfEmpty] synced" << banks.size() << "banks"; + } else { + qWarning() << "[syncBanksIfEmpty] no banks in response"; + } +} + +void NetworkService::syncDevicesFromServer(const QString &desktopId) { + if (desktopId.isEmpty()) return; + + const QJsonValue devicesResult = getJson(m_apiBase + m_pathDevice + desktopId, true); + if (devicesResult.isUndefined() || devicesResult.isNull() || !devicesResult.isArray()) { + qWarning() << "[syncDevicesFromServer] failed to get devices for desktop" << desktopId; + return; + } + + const QJsonArray devicesArray = devicesResult.toArray(); + QSet processedNames; // защита от дублей с сервера + + for (const QJsonValue &devVal : devicesArray) { + const QJsonObject dev = devVal.toObject(); + const QString apiId = dev["id"].toString(); + const QString name = dev["name"].toString(); + if (apiId.isEmpty()) continue; + + // Пропускаем дубли по name + if (processedNames.contains(name)) { + qDebug() << "[syncDevicesFromServer] skipping duplicate:" << apiId << name; + continue; + } + processedNames.insert(name); + + // Проверяем: есть ли уже устройство с таким name в локальной БД + const DeviceInfo existing = DeviceDAO::findByName(name); + if (!existing.id.isEmpty() && !existing.apiId.isEmpty()) { + continue; + } + + if (!existing.id.isEmpty() && existing.apiId.isEmpty()) { + DeviceDAO::updateApiId(existing.id, apiId); + qDebug() << "[syncDevicesFromServer] linked apiId" << apiId << "to device" << existing.id; + continue; + } + + // Устройства нет в БД — создаём одну запись (DeviceScreener потом подхватит) + DeviceInfo deviceInfo; + deviceInfo.apiId = apiId; + deviceInfo.name = name; + deviceInfo.status = dev["status"].toString(); + deviceInfo.screenWidth = dev["screen_width"].toInt(); + deviceInfo.screenHeight = dev["screen_height"].toInt(); + deviceInfo.battery = dev["battery"].toInt(); + deviceInfo.id = apiId; + + if (DeviceDAO::upsertDevice(deviceInfo)) { + qDebug() << "[syncDevicesFromServer] restored device:" << apiId << name; + } else { + qWarning() << "[syncDevicesFromServer] failed to restore device:" << apiId; + } + } +} + void NetworkService::syncCurrentProfile() { // 1. Определяем онлайн-устройства по БД (DeviceScreener обновит позже) const QList allDevices = DeviceDAO::getAllDevices(false); @@ -492,8 +837,8 @@ void NetworkService::syncCurrentProfile() { const QString serverBpState = bp["state"].toString(); // "enabled" / "disabled" // Находим applications с этим bank_profile_id - const QList apps = ApplicationDAO::findAllByBankProfileId(bpId); - for (const ApplicationInfo &app : apps) { + const QList apps = BankProfileDAO::findAllByBankProfileId(bpId); + for (const BankProfileInfo &app : apps) { const bool deviceOnline = onlineDeviceIds.contains(app.deviceId); const QString localStatus = app.status; // "active" / "off" const QString serverStatus = (serverBpState == "disabled") ? "off" : "active"; @@ -521,11 +866,11 @@ void NetworkService::syncCurrentProfile() { const QString matId = mat["id"].toString(); const QString serverMatState = mat["state"].toString(); - const AccountInfo account = AccountDAO::findByMaterialId(matId); + const MaterialInfo account = MaterialDAO::findByMaterialId(matId); if (account.id < 0) continue; // не найден в локальной БД const bool deviceOnline = onlineDeviceIds.contains(account.deviceId); - const QString localStatus = accountStatusToString(account.status); + const QString localStatus = materialStatusToString(account.status); const QString serverStatus = (serverMatState == "disabled") ? "off" : "active"; if (!deviceOnline) { @@ -559,7 +904,7 @@ void NetworkService::postBankProfile() { const QString bankProfileId = data["id"].toString(); const int appId = m_extraData.value("app_id").toInt(); if (!bankProfileId.isEmpty() && appId > 0) { - ApplicationDAO::updateBankProfileId(appId, bankProfileId); + BankProfileDAO::updateBankProfileId(appId, bankProfileId); qDebug() << "[NetworkService] postBankProfile saved bank_profile_id:" << bankProfileId; } } @@ -616,7 +961,7 @@ void NetworkService::postMaterial() { 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); + MaterialDAO::updateMaterialId(accountId, materialId); } qDebug() << "[NetworkService] postMaterial ok, card:" << body["card_number"].toString() << "material_id:" << materialId; } @@ -802,7 +1147,7 @@ void NetworkService::getTasks(const QStringList &materialIds) { QUrl url(m_apiBase + m_pathGetTasks); QUrlQuery urlQuery; - const QStringList ids = materialIds.isEmpty() ? AccountDAO::getActiveMaterialIds() : materialIds; + const QStringList ids = materialIds.isEmpty() ? MaterialDAO::getActiveMaterialIds() : materialIds; if (ids.isEmpty()) { m_fetchingTasks = false; return; @@ -903,8 +1248,62 @@ void NetworkService::start() { scheduleNextRefresh(); } +void NetworkService::deleteDeviceFromServer() { + if (!ensureValidToken()) { emit finished(); return; } + + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + if (device.apiId.isEmpty()) { + qWarning() << "[deleteDeviceFromServer] no api_id for device" << m_deviceId; + // Удаляем только локально + DeviceDAO::deleteDevice(m_deviceId); + emit finished(); + return; + } + + const QJsonValue result = deleteJson(m_apiBase + m_pathDevice + device.apiId, true); + if (result.isUndefined()) { + qWarning() << "[deleteDeviceFromServer] failed to delete device on server:" << device.apiId; + } else { + qDebug() << "[deleteDeviceFromServer] deleted device on server:" << device.apiId; + } + + DeviceDAO::deleteDevice(m_deviceId); + emit finished(); +} + +void NetworkService::disableAllProfilesOnServer() { + if (!ensureValidToken()) return; + + const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true); + if (profileResult.isUndefined() || profileResult.isNull()) return; + + const QJsonArray bankProfiles = profileResult.toObject()["bank_profiles"].toArray(); + for (const QJsonValue &bpVal : bankProfiles) { + const QJsonObject bp = bpVal.toObject(); + const QString bpId = bp["id"].toString(); + const QString bpState = bp["state"].toString(); + if (bpId.isEmpty() || bpState == "disabled") continue; + + patchJson(m_apiBase + m_pathBankProfile + "/" + bpId + "/false", QJsonObject(), true); + qDebug() << "[shutdown] disabled bank_profile" << bpId; + + const QJsonArray materials = bp["materials"].toArray(); + for (const QJsonValue &matVal : materials) { + const QJsonObject mat = matVal.toObject(); + const QString matId = mat["id"].toString(); + const QString matState = mat["state"].toString(); + if (matId.isEmpty() || matState == "disabled") continue; + + patchJson(m_apiBase + m_pathMaterial + "/" + matId + "/false", QJsonObject(), true); + qDebug() << "[shutdown] disabled material" << matId; + } + } +} + void NetworkService::stop() { m_running = false; - // Эмитируем finished() в потоке NetworkService, чтобы корректно завершить QThread - QMetaObject::invokeMethod(this, [this]() { emit finished(); }, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, [this]() { + disableAllProfilesOnServer(); + emit finished(); + }, Qt::QueuedConnection); } diff --git a/services/net/NetworkService.h b/services/net/NetworkService.h index bbd9a47..fed0cad 100644 --- a/services/net/NetworkService.h +++ b/services/net/NetworkService.h @@ -60,6 +60,8 @@ public slots: void start(); void stop(); + void disableAllProfilesOnServer(); + void deleteDeviceFromServer(); signals: void finished(); @@ -103,8 +105,12 @@ private: QJsonValue getJson(const QString &path, bool withAuth = false); QJsonValue postJson(const QString &path, const QJsonObject &body, bool withAuth = false); QJsonValue patchJson(const QString &path, const QJsonObject &body, bool withAuth = false); + QJsonValue deleteJson(const QString &path, bool withAuth = false); bool refreshToken(); bool ensureValidToken(); void scheduleNextRefresh(); + void syncDevicesFromServer(const QString &desktopId); + void syncBankProfilesFromServer(); + void syncBanksIfEmpty(); }; diff --git a/views/MainWindow.cpp b/views/MainWindow.cpp index 1f4c8ef..e07e036 100644 --- a/views/MainWindow.cpp +++ b/views/MainWindow.cpp @@ -49,14 +49,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { auto *bankListAction = new QAction("Банки", this); auto *tutorialPageAction = new QAction("Инструкция", this); auto *eventAction = new QAction("События", this); - auto *errorLogAction = new QAction("Ошибки", this); auto *fileLogAction = new QAction("Логи", this); menu->addAction(devicesPageAction); // menu->addAction(accountPageAction); menu->addAction(bankListAction); menu->addAction(eventAction); menu->addAction(tutorialPageAction); - menu->addAction(errorLogAction); menu->addAction(fileLogAction); connect(devicesPageAction, &QAction::triggered, this, [=]() { @@ -98,15 +96,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_eventWindow->activateWindow(); }); - connect(errorLogAction, &QAction::triggered, this, [=]() { - if (!m_logWindow) { - m_logWindow = new LogWindow(this); - } - m_logWindow->show(); - m_logWindow->raise(); - m_logWindow->activateWindow(); - }); - connect(fileLogAction, &QAction::triggered, this, [=]() { if (!m_fileLogWindow) { m_fileLogWindow = new FileLogWindow(this); diff --git a/views/MainWindow.h b/views/MainWindow.h index de5690f..84e66a7 100644 --- a/views/MainWindow.h +++ b/views/MainWindow.h @@ -32,7 +32,6 @@ private: QWidget *m_accountsWindow = nullptr; QWidget *m_tutorialWindow = nullptr; QWidget *m_bankListWindow = nullptr; - QWidget *m_logWindow = nullptr; QWidget *m_fileLogWindow = nullptr; QWidget *m_eventWindow = nullptr; diff --git a/views/bank/AccountSettingsWindow.cpp b/views/bank/AccountSettingsWindow.cpp index 0612889..12250bc 100644 --- a/views/bank/AccountSettingsWindow.cpp +++ b/views/bank/AccountSettingsWindow.cpp @@ -19,15 +19,15 @@ #include "black/LoginAndCheckAccountsScript.h" #include "black/GetProfileInfoScript.h" #include "black/GetCardInfoScript.h" -#include "dao/AccountDAO.h" -#include "dao/ApplicationDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" #include "bank/TransactionWindow.h" #include "device/EditBankAccountDialog.h" #include "device/EditBankDialog.h" #include "net/NetworkService.h" AccountSettingsWindow::AccountSettingsWindow( - QWidget *parent, QString deviceId, QString deviceName, const ApplicationInfo &app + QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app ) : QWidget(parent, Qt::Window | Qt::WindowTitleHint @@ -90,7 +90,7 @@ AccountSettingsWindow::AccountSettingsWindow( const QString comment = dlg.secondValue(); bool on = dlg.toggled(); - if (!ApplicationDAO::update(m_applicationInfo.id, pinCode, comment, on ? "active" : "off")) { + if (!BankProfileDAO::update(m_applicationInfo.id, pinCode, comment, on ? "active" : "off")) { qDebug() << "Cant update bank data"; } else { m_applicationInfo.pinCode = pinCode; @@ -132,6 +132,17 @@ AccountSettingsWindow::AccountSettingsWindow( m_profileLabel->setTextFormat(Qt::RichText); m_profileLabel->setStyleSheet("margin-bottom: 8px;"); profileRow->addWidget(m_profileLabel); + + // Статус синхронизации с сервером + const QString syncText = m_applicationInfo.bankProfileId.isEmpty() + ? "● Не синхронизирован" + : "● Синхронизирован"; + auto *syncLabel = new QLabel(syncText); + syncLabel->setTextFormat(Qt::RichText); + syncLabel->setStyleSheet("margin-bottom: 8px; margin-left: 16px;"); + profileRow->addWidget(syncLabel); + profileRow->addStretch(); + layout->addLayout(profileRow); QPushButton *soloButton = new QPushButton("Запустить проверку пин-кода"); @@ -320,35 +331,6 @@ AccountSettingsWindow::AccountSettingsWindow( layout->addWidget(tableWidget); - QPushButton *addAccountButton = new QPushButton("Добавить аккаунт"); - addAccountButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); - AccountInfo account; - connect(addAccountButton, &QPushButton::clicked, this, [this, tableWidget, account]() { - EditBankAccountDialog dlg(account, this); - if (dlg.exec() == QDialog::Accepted) { - const QString cardNumber = dlg.cardNumberValue(); - const QString currency = dlg.currencyValue(); - bool on = dlg.toggled(); - - AccountInfo accountInfo; - accountInfo.currency = currency; - accountInfo.description = cardNumber; - accountInfo.amount = 0; - accountInfo.appCode = m_applicationInfo.code; - accountInfo.deviceId = m_deviceId; - accountInfo.status = on ? AccountStatus::Active : AccountStatus::Off; - accountInfo.lastNumbers = cardNumber.right(4); - if (!AccountDAO::upsertAccount(accountInfo)) { - qDebug() << "Cant insert bank account data"; - } else { - reloadContent(tableWidget); - tableWidget->resizeColumnsToContents(); - updateAccountData(accountInfo); - } - } - }); - - layout->addWidget(addAccountButton); tableWidget->resizeRowsToContents(); @@ -365,11 +347,22 @@ AccountSettingsWindow::AccountSettingsWindow( void AccountSettingsWindow::startCheckPinCode(const QString &appCode) { QThread *thread = new QThread(nullptr); + const QString deviceId = m_deviceId; + + auto onResult = [deviceId, appCode](const QString &result) { + if (result.isEmpty()) { + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); + if (app.id >= 0) { + BankProfileDAO::updatePinCodeStatus(app.id, "yes", ""); + } + } + }; if (appCode == "ozon") { auto *worker = new Ozon::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult); connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); @@ -377,6 +370,7 @@ void AccountSettingsWindow::startCheckPinCode(const QString &appCode) { auto *worker = new Black::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start); + connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult); connect(worker, &Black::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); @@ -438,7 +432,7 @@ void AccountSettingsWindow::startGetCardInfo(const QString &appCode) { void AccountSettingsWindow::startGetLastTransactions(const QString &appCode, int maxCount) { if (appCode == "ozon") { QThread *thread = new QThread(nullptr); - auto *worker = new Ozon::GetLastTransactionsScript(m_deviceId, appCode, maxCount, nullptr); + auto *worker = new Ozon::GetLastTransactionsScript(m_deviceId, appCode, maxCount); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Ozon::GetLastTransactionsScript::start); connect(worker, &Ozon::GetLastTransactionsScript::finished, thread, &QThread::quit); @@ -458,7 +452,7 @@ void AccountSettingsWindow::startGetLastTransactions(const QString &appCode, int } void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) { - const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code); + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code); if (app.bankProfileId.isEmpty()) { qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << code; return; @@ -477,7 +471,7 @@ void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString thread->start(); } -void AccountSettingsWindow::updateAccountData(const AccountInfo &account) { +void AccountSettingsWindow::updateAccountData(const MaterialInfo &account) { QJsonArray list; QJsonObject obj; obj["appName"] = account.appCode; @@ -485,7 +479,7 @@ void AccountSettingsWindow::updateAccountData(const AccountInfo &account) { obj["amount"] = account.amount; obj["description"] = account.description; obj["currency"] = account.currency; - obj["status"] = accountStatusToString(account.status); + obj["status"] = materialStatusToString(account.status); list.append(obj); @@ -502,11 +496,11 @@ void AccountSettingsWindow::updateAccountData(const AccountInfo &account) { } void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) { - QList accounts = AccountDAO::getAccounts(m_deviceId, m_applicationInfo.code); + QList accounts = MaterialDAO::getAccounts(m_deviceId, m_applicationInfo.code); tableWidget->clearContents(); tableWidget->setRowCount(accounts.length()); - tableWidget->setColumnCount(8); + tableWidget->setColumnCount(9); tableWidget->setHorizontalHeaderLabels({ "Время обновления", "Номер", @@ -514,6 +508,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) { "Сумма", "Валюта", "Статус", + "Сервер", "", "" }); @@ -526,7 +521,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) { for (int k = 0; k < accounts.size(); ++k) { - const AccountInfo &account = accounts[k]; + const MaterialInfo &account = accounts[k]; QTableWidgetItem *time = new QTableWidgetItem(account.updateTime.toString("dd.MM.yyyy HH:mm:ss")); time->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); @@ -554,18 +549,24 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) { currency->setFlags(currency->flags() & ~Qt::ItemIsEditable); tableWidget->setItem(k, 4, currency); - QTableWidgetItem *status = new QTableWidgetItem(accountStatusToUiString(account.status)); + QTableWidgetItem *status = new QTableWidgetItem(materialStatusToUiString(account.status)); status->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); status->setFlags(status->flags() & ~Qt::ItemIsEditable); - if (account.status != AccountStatus::Active) { + if (account.status != MaterialStatus::Active) { status->setForeground(QBrush(Qt::red)); } tableWidget->setItem(k, 5, status); + auto *syncItem = new QTableWidgetItem(account.materialId.isEmpty() ? "Не синхр." : "Синхр."); + syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + syncItem->setForeground(account.materialId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen)); + syncItem->setFlags(syncItem->flags() & ~Qt::ItemIsEditable); + tableWidget->setItem(k, 6, syncItem); + auto *deleteBankAccountBtn = new QPushButton("Удалить", tableWidget); deleteBankAccountBtn->setStyleSheet("background-color: red; color: white;"); - tableWidget->setCellWidget(k, 6, deleteBankAccountBtn); + tableWidget->setCellWidget(k, 7, deleteBankAccountBtn); connect(deleteBankAccountBtn, &QPushButton::clicked, this, [this, tableWidget, account]() { QMessageBox msgBox(this); @@ -576,7 +577,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) { msgBox.setDefaultButton(cancelBtn); msgBox.exec(); if (msgBox.clickedButton() == deleteBtn) { - if (AccountDAO::deleteAccount(account.id)) { + if (MaterialDAO::deleteAccount(account.id)) { reloadContent(tableWidget); tableWidget->resizeColumnsToContents(); } else { @@ -587,7 +588,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) { auto *transactionsBtn = new QPushButton("Транзакции", tableWidget); transactionsBtn->setStyleSheet("background-color: #1565C0; color: white;"); - tableWidget->setCellWidget(k, 7, transactionsBtn); + tableWidget->setCellWidget(k, 8, transactionsBtn); connect(transactionsBtn, &QPushButton::clicked, this, [this, account]() { auto *txWindow = new TransactionWindow(account.id, this); diff --git a/views/bank/AccountSettingsWindow.h b/views/bank/AccountSettingsWindow.h index 81a30a0..a0f36dd 100644 --- a/views/bank/AccountSettingsWindow.h +++ b/views/bank/AccountSettingsWindow.h @@ -3,19 +3,19 @@ #include #include -#include "db/AccountInfo.h" -#include "db/ApplicationInfo.h" +#include "db/MaterialInfo.h" +#include "db/BankProfileInfo.h" class AccountSettingsWindow final : public QWidget { Q_OBJECT public: - AccountSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const ApplicationInfo &app); + AccountSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app); private: QString m_deviceId; QString m_deviceName; - ApplicationInfo m_applicationInfo; + BankProfileInfo m_applicationInfo; QLabel *m_statusLabel; QLabel *m_commentLabel; @@ -33,5 +33,5 @@ private: void sendAppStatus(const QString &deviceId, const QString &code, const QString &status); - void updateAccountData(const AccountInfo &account); + void updateAccountData(const MaterialInfo &account); }; diff --git a/views/bank/AccountWindow.cpp b/views/bank/AccountWindow.cpp index 391f998..aa8586c 100644 --- a/views/bank/AccountWindow.cpp +++ b/views/bank/AccountWindow.cpp @@ -7,7 +7,7 @@ #include #include "TransactionWindow.h" -#include "dao/AccountDAO.h" +#include "dao/MaterialDAO.h" #include "dao/DeviceDAO.h" AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) { @@ -30,7 +30,7 @@ AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) { QList devices = DeviceDAO::getAllDevices(false); for (int i = 0; i < devices.size(); ++i) { DeviceInfo device = devices[i]; - QList accounts = AccountDAO::getAccounts(device.id, "ozon"); + QList accounts = MaterialDAO::getAccounts(device.id, "ozon"); // Текст перед первой таблицей QString title = QString("%1 [%2] | %3").arg(device.name, device.id, device.status); QLabel *label = new QLabel(title, this); @@ -54,14 +54,14 @@ AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) { for (int k = 0; k < accounts.size(); ++k) { - AccountInfo account = accounts[k]; + MaterialInfo account = accounts[k]; QTableWidgetItem *item = new QTableWidgetItem("*" + account.lastNumbers); item->setData(Qt::UserRole, account.id); tableWidget->setItem(k, 0, item); tableWidget->setItem(k, 1, new QTableWidgetItem(QString::number(account.amount))); tableWidget->setItem(k, 2, new QTableWidgetItem(account.updateTime.toString("dd.MM.yyyy HH:mm:ss"))); tableWidget->setItem(k, 3, new QTableWidgetItem(account.description)); - tableWidget->setItem(k, 4, new QTableWidgetItem(accountStatusToString(account.status))); + tableWidget->setItem(k, 4, new QTableWidgetItem(materialStatusToString(account.status))); totalHeight += tableWidget->rowHeight(k); } tableWidget->setFixedHeight(totalHeight + 2); diff --git a/views/bank/BankSettingsWindow.cpp b/views/bank/BankSettingsWindow.cpp index 38f1c3f..87f5a14 100644 --- a/views/bank/BankSettingsWindow.cpp +++ b/views/bank/BankSettingsWindow.cpp @@ -15,8 +15,9 @@ #include #include -#include "dao/ApplicationDAO.h" -#include "db/ApplicationInfo.h" +#include "dao/BankProfileDAO.h" +#include "dao/EventDAO.h" +#include "db/BankProfileInfo.h" #include "net/NetworkService.h" #include "common/PaddedItemDelegate.h" #include "device/EditBankDialog.h" @@ -29,7 +30,7 @@ QTableWidgetItem *BankSettingsWindow::createTableWidget(const QString &text) { return item; } -QString parseStatus(const ApplicationInfo &app) { +QString parseStatus(const BankProfileInfo &app) { if (!app.install) { return "не установлен"; } @@ -47,11 +48,22 @@ QString parseStatus(const ApplicationInfo &app) { void BankSettingsWindow::startCheckPinCode(const QString &appCode) { QThread *thread = new QThread(nullptr); + const QString deviceId = m_deviceId; + + auto onResult = [deviceId, appCode](const QString &result) { + if (result.isEmpty()) { + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); + if (app.id >= 0) { + BankProfileDAO::updatePinCodeStatus(app.id, "yes", ""); + } + } + }; if (appCode == "ozon") { auto *worker = new Ozon::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult); connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); @@ -59,6 +71,7 @@ void BankSettingsWindow::startCheckPinCode(const QString &appCode) { auto *worker = new Black::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start); + connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult); connect(worker, &Black::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QObject::deleteLater); @@ -70,13 +83,45 @@ void BankSettingsWindow::startCheckPinCode(const QString &appCode) { thread->start(); } +static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName) { + QJsonObject errData; + errData["status"] = "error"; + errData["description"] = "Bank profile disabled"; + + QJsonObject obj; + obj["type"] = type; + obj["material_id"] = materialId; + obj["bank_name"] = bankName; + obj["data"] = errData; + + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->setPayload(QJsonDocument(obj).toJson()); + QObject::connect(thread, &QThread::started, net, &NetworkService::postTaskResult); + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + void sendAppStatus(const QString &deviceId, const QString &code, const QString &status) { - const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code); + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code); if (app.bankProfileId.isEmpty()) { qWarning() << "[BankSettings] sendAppStatus: no bankProfileId for" << code; return; } + // При выключении — отменяем ожидающие задачи + if (status != "active") { + const QList cancelled = EventDAO::cancelWaitingByBank(deviceId, code); + for (const EventInfo &e : cancelled) { + sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName); + EventDAO::markSentToServer(e.id); + qDebug() << "[BankSettings] cancelled task" << e.id << eventTypeToString(e.type); + } + } + const bool enable = (status == "active"); auto *thread = new QThread; auto *net = new NetworkService; @@ -91,15 +136,16 @@ void sendAppStatus(const QString &deviceId, const QString &code, const QString & } void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { - QList applications = ApplicationDAO::getApplicationsByDeviceId(m_deviceId); + QList applications = BankProfileDAO::getApplicationsByDeviceId(m_deviceId); tableWidget->setRowCount(applications.length()); - tableWidget->setColumnCount(7); + tableWidget->setColumnCount(8); tableWidget->setHorizontalHeaderLabels({ "", "Имя", "Пин-код", "Статус", + "Сервер", "Комментарий", "", "" @@ -109,17 +155,18 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { tableWidget->setTextElideMode(Qt::ElideNone); tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); - for (int i = 0; i < 7; ++i) { + for (int i = 0; i < 8; ++i) { tableWidget->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents); } for (int i = 0; i < applications.length(); ++i) { tableWidget->verticalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents); } - tableWidget->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Stretch); + tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch); + tableWidget->setColumnWidth(4, 140); for (int k = 0; k < applications.size(); ++k) { - const ApplicationInfo &app = applications[k]; + const BankProfileInfo &app = applications[k]; auto *icon = new QLabel(tableWidget); @@ -151,20 +198,23 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { tableWidget->setItem(k, 3, createTableWidget(parseStatus(app))); - // tableWidget->setItem(k, 4, createTableWidget(app.comment)); + // Статус синхронизации с сервером + auto *syncItem = new QTableWidgetItem(app.bankProfileId.isEmpty() ? "Не синхронизирован" : "Синхронизирован"); + syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + syncItem->setForeground(app.bankProfileId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen)); + tableWidget->setItem(k, 4, syncItem); QTableWidgetItem *item = new QTableWidgetItem(app.comment); item->setTextAlignment(Qt::AlignLeft | Qt::AlignTop); item->setFlags(item->flags() & ~Qt::ItemIsEditable); - // item->setSizeHint(QSize(100, app.comment.length() / 2)); // Минимальная высота ячейки для переноса - tableWidget->setItem(k, 4, item); + tableWidget->setItem(k, 5, item); if (app.install) { if (!app.pinCode.isEmpty() && !app.pinCodeChecked) { auto *pinCodeBtn = new QPushButton(" Проверить пин-код ", tableWidget); pinCodeBtn->setProperty("btnClass", "pincode"); - tableWidget->setCellWidget(k, 5, pinCodeBtn); + tableWidget->setCellWidget(k, 6, pinCodeBtn); connect(pinCodeBtn, &QPushButton::clicked, this, [this, app]() { QMessageBox msgBox(this); @@ -187,7 +237,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { deleteBtn->setProperty("btnClass", "delete"); // deleteBtn->setStyleSheet("padding: 4px 8px;"); // deleteBtn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); - tableWidget->setCellWidget(k, 5, deleteBtn); + tableWidget->setCellWidget(k, 6, deleteBtn); connect(deleteBtn, &QPushButton::clicked, this, [this,tableWidget, app]() { QMessageBox msgBox(this); @@ -201,7 +251,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { msgBox.exec(); if (msgBox.clickedButton() == deleteBtn) { - if (!ApplicationDAO::update(app.id, app.pinCode, app.comment, "off")) { + if (!BankProfileDAO::update(app.id, app.pinCode, app.comment, "off")) { qDebug() << "Cant update bank data"; } else { sendAppStatus(app.deviceId, app.code, "off"); @@ -222,7 +272,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { auto *btn = new QPushButton(" Редактировать ", tableWidget); btn->setProperty("btnClass", "edit"); // btn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); - tableWidget->setCellWidget(k, 6, btn); + tableWidget->setCellWidget(k, 7, btn); connect(btn, &QPushButton::clicked, this, [this, tableWidget, k,app]() { BankEditDialog dlg(app, this); if (dlg.exec() == QDialog::Accepted) { @@ -230,7 +280,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { const QString comment = dlg.secondValue(); bool on = dlg.toggled(); - if (!ApplicationDAO::update(app.id, pinCode, comment, on ? "active" : "off")) { + if (!BankProfileDAO::update(app.id, pinCode, comment, on ? "active" : "off")) { qDebug() << "Cant update bank data"; } else { sendAppStatus(app.deviceId, app.code, on ? "active" : "off"); @@ -246,7 +296,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { } } -BankSettingsWindow::BankSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const ApplicationInfo &app) +BankSettingsWindow::BankSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app) : QWidget(parent, Qt::Window | Qt::WindowTitleHint diff --git a/views/bank/BankSettingsWindow.h b/views/bank/BankSettingsWindow.h index 8da1544..e6ca4f2 100644 --- a/views/bank/BankSettingsWindow.h +++ b/views/bank/BankSettingsWindow.h @@ -1,13 +1,13 @@ #pragma once #include #include -#include "db/ApplicationInfo.h" +#include "db/BankProfileInfo.h" class BankSettingsWindow final : public QWidget { Q_OBJECT public: - BankSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const ApplicationInfo &app); + BankSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app); private: QString m_deviceId; diff --git a/views/bank/TransactionWindow.cpp b/views/bank/TransactionWindow.cpp index 6148b49..b141339 100644 --- a/views/bank/TransactionWindow.cpp +++ b/views/bank/TransactionWindow.cpp @@ -11,7 +11,7 @@ #include #include -#include "dao/AccountDAO.h" +#include "dao/MaterialDAO.h" #include "dao/TransactionDAO.h" class AmountDelegate : public QStyledItemDelegate { @@ -67,7 +67,7 @@ TransactionWindow::TransactionWindow(const int accountId, QWidget *parent): QDia scrollArea->setStyleSheet("border: none;"); // Убираем рамку layout->setAlignment(Qt::AlignTop); - AccountInfo account = AccountDAO::getAccountById(accountId); + MaterialInfo account = MaterialDAO::getAccountById(accountId); QList transactions = TransactionDAO::getTransactions(accountId); QTableWidget *tableWidget = new QTableWidget(this); diff --git a/views/device/DeviceSettingsWindow.cpp b/views/device/DeviceSettingsWindow.cpp index 15753a3..cbf8442 100644 --- a/views/device/DeviceSettingsWindow.cpp +++ b/views/device/DeviceSettingsWindow.cpp @@ -7,11 +7,12 @@ #include #include +#include #include "bank/AccountSettingsWindow.h" -#include "dao/ApplicationDAO.h" -#include "db/ApplicationInfo.h" +#include "dao/BankProfileDAO.h" +#include "db/BankProfileInfo.h" -static QString formatStatus(const ApplicationInfo &app) { +static QString formatStatus(const BankProfileInfo &app) { if (!app.install) return "не установлен"; if (app.status == "active") { if (!app.pinCodeChecked) @@ -63,7 +64,29 @@ DeviceSettingsWindow::DeviceSettingsWindow(QWidget *parent, const QString &devic } void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) { - QList apps = ApplicationDAO::getApplicationsByDeviceId(m_deviceId); + QList apps = BankProfileDAO::getAllApplicationsByDeviceId(m_deviceId); + + // Дополняем список банками из config.ini, которых нет в БД + const QSettings configSettings("config.ini", QSettings::IniFormat); + const QStringList banks = configSettings.value("apps/list").toStringList(); + QSet existingCodes; + for (const BankProfileInfo &a : apps) { + existingCodes.insert(a.code); + } + for (const QString &rawBank : banks) { + const QString bank = rawBank.trimmed(); + if (bank.isEmpty() || existingCodes.contains(bank)) continue; + BankProfileInfo stub; + stub.id = -1; + stub.deviceId = m_deviceId; + stub.code = bank; + stub.name = configSettings.value(bank + "/name", bank).toString(); + stub.package = configSettings.value(bank + "/android_package_name").toString(); + stub.currency = configSettings.value(bank + "/currency").toString(); + stub.status = "off"; + stub.install = false; + apps.append(stub); + } tableWidget->clearContents(); tableWidget->setRowCount(apps.size()); @@ -76,7 +99,7 @@ void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) { tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch); for (int k = 0; k < apps.size(); ++k) { - const ApplicationInfo &app = apps[k]; + const BankProfileInfo &app = apps[k]; // Иконка auto *icon = new QLabel(tableWidget); diff --git a/views/device/DeviceWidget.cpp b/views/device/DeviceWidget.cpp index fbce54e..66e03cc 100644 --- a/views/device/DeviceWidget.cpp +++ b/views/device/DeviceWidget.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -13,14 +15,15 @@ #include "bank/AccountSettingsWindow.h" #include "bank/BankSettingsWindow.h" #include "DeviceSettingsWindow.h" -#include "dao/ApplicationDAO.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" -#include "db/ApplicationInfo.h" +#include "dao/EventDAO.h" +#include "db/BankProfileInfo.h" #include "net/NetworkService.h" #include -QString getColorByStatus(const ApplicationInfo &app) { +QString getColorByStatus(const BankProfileInfo &app) { if (!app.install) { return "black"; } @@ -36,13 +39,44 @@ QString getColorByStatus(const ApplicationInfo &app) { } } +static void sendTaskErrorToServer(const QString &type, const QString &materialId, const QString &bankName) { + QJsonObject errData; + errData["status"] = "error"; + errData["description"] = "Bank profile disabled"; + + QJsonObject obj; + obj["type"] = type; + obj["material_id"] = materialId; + obj["bank_name"] = bankName; + obj["data"] = errData; + + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->setPayload(QJsonDocument(obj).toJson()); + QObject::connect(thread, &QThread::started, net, &NetworkService::postTaskResult); + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + void sendBankStatus(const QString &deviceId, const QString &code, const QString &status) { - const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code); + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code); if (app.bankProfileId.isEmpty()) { qWarning() << "[DeviceWidget] sendBankStatus: no bankProfileId for" << code; return; } + // При выключении — отменяем ожидающие задачи + if (status != "active") { + const QList cancelled = EventDAO::cancelWaitingByBank(deviceId, code); + for (const EventInfo &e : cancelled) { + sendTaskErrorToServer(eventTypeToString(e.type), e.externalId, e.bankName); + EventDAO::markSentToServer(e.id); + } + } + const bool enable = (status == "active"); auto *thread = new QThread; auto *net = new NetworkService; @@ -56,7 +90,7 @@ void sendBankStatus(const QString &deviceId, const QString &code, const QString thread->start(); } -void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline) { +void DeviceWidget::buildBankRow(const BankProfileInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline) { rowWidget->setContentsMargins(0, 0, 0, 0); rowWidget->setStyleSheet( "background-color: rgba(0, 0, 0, 0.6);" // полупрозрачный фон @@ -78,7 +112,16 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, ); - auto *label = new QLabel(app.name, parent); + QString labelText; + if (!app.fullName.isEmpty()) { + labelText = app.fullName; + if (!app.phone.isEmpty()) { + labelText += "\n" + app.phone; + } + } else { + labelText = app.name; + } + auto *label = new QLabel(labelText, parent); label->setStyleSheet( "background-color: transparent;" "margin: 0px;" @@ -129,7 +172,7 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, msgBox.exec(); if (msgBox.clickedButton() == confirmBtn) { - if (!ApplicationDAO::update(app.id, app.pinCode, app.comment, newStatus)) { + if (!BankProfileDAO::update(app.id, app.pinCode, app.comment, newStatus)) { qDebug() << "Cant update bank data"; } else { sendBankStatus(app.deviceId, app.code, newStatus); @@ -175,7 +218,7 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, // hbox->addStretch(); } -void DeviceWidget::setOpenBanksSettings(QPushButton *button, const ApplicationInfo &app) { +void DeviceWidget::setOpenBanksSettings(QPushButton *button, const BankProfileInfo &app) { QString deviceId = m_device.id; QString deviceName = m_device.name; connect(button, &QPushButton::clicked, this, [deviceId, deviceName, app]() { @@ -219,6 +262,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p statusLabel->setStyleSheet( "background-color: rgba(0, 0, 0, 0.5);" "color: white;" + "font-size: 10px;" ); layout->addWidget(statusLabel, 0, 0, Qt::AlignTop | Qt::AlignLeft); @@ -274,7 +318,15 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p msgBox.exec(); if (msgBox.clickedButton() == confirmBtn) { - DeviceDAO::deleteDevice(deviceId); + auto *thread = new QThread; + auto *net = new NetworkService; + net->setDeviceId(deviceId); + net->moveToThread(thread); + QObject::connect(thread, &QThread::started, net, &NetworkService::deleteDeviceFromServer); + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); } }); @@ -307,8 +359,8 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p const bool isOffline = device.status != "device"; - QList apps = ApplicationDAO::getApplicationsByDeviceId(device.id); - for (ApplicationInfo &app: apps) { + QList apps = BankProfileDAO::getApplicationsByDeviceId(device.id); + for (BankProfileInfo &app: apps) { if (app.install) { auto *rowWidget = new QWidget(appsView); buildBankRow(app, rowWidget, appsView, isOffline); diff --git a/views/device/DeviceWidget.h b/views/device/DeviceWidget.h index 34985f7..d1958cb 100644 --- a/views/device/DeviceWidget.h +++ b/views/device/DeviceWidget.h @@ -3,7 +3,7 @@ #include #include -#include "db/ApplicationInfo.h" +#include "db/BankProfileInfo.h" #include "db/DeviceInfo.h" class DeviceWidget final : public QWidget { @@ -15,9 +15,9 @@ public: private: const DeviceInfo m_device; - void buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline = false); + void buildBankRow(const BankProfileInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline = false); - void setOpenBanksSettings(QPushButton *button, const ApplicationInfo &app); + void setOpenBanksSettings(QPushButton *button, const BankProfileInfo &app); signals: void deleteRequested(const QString &deviceId); diff --git a/views/device/EditBankAccountDialog.h b/views/device/EditBankAccountDialog.h index c2141ef..aea73f1 100644 --- a/views/device/EditBankAccountDialog.h +++ b/views/device/EditBankAccountDialog.h @@ -7,13 +7,13 @@ #include #include -#include "db/AccountInfo.h" +#include "db/MaterialInfo.h" class EditBankAccountDialog final : public QDialog { Q_OBJECT public: - explicit EditBankAccountDialog(const AccountInfo &account, QWidget *parent = nullptr) : QDialog(parent) { + explicit EditBankAccountDialog(const MaterialInfo &account, QWidget *parent = nullptr) : QDialog(parent) { if (account.id > 0) { setWindowTitle("Настройка аккаунта *" + account.lastNumbers); } else { @@ -36,7 +36,7 @@ public: currency->setCurrentText(account.currency); toggle = new QCheckBox(tr("В работе"), this); - toggle->setChecked(account.status == AccountStatus::Active); + toggle->setChecked(account.status == MaterialStatus::Active); form->addRow(tr("Номер карты:"), cardNumber); form->addRow(tr("Валюта:"), currency); diff --git a/views/device/EditBankDialog.h b/views/device/EditBankDialog.h index 31013fb..61f5bc2 100644 --- a/views/device/EditBankDialog.h +++ b/views/device/EditBankDialog.h @@ -5,13 +5,13 @@ #include #include #include -#include "db/ApplicationInfo.h" +#include "db/BankProfileInfo.h" class BankEditDialog final : public QDialog { Q_OBJECT public: - explicit BankEditDialog(const ApplicationInfo &app, QWidget *parent = nullptr) : QDialog(parent) { + explicit BankEditDialog(const BankProfileInfo &app, QWidget *parent = nullptr) : QDialog(parent) { setWindowTitle("Настройка " + app.name); setModal(true); diff --git a/views/event/EventWindow.cpp b/views/event/EventWindow.cpp index e4a12f8..7f40a81 100644 --- a/views/event/EventWindow.cpp +++ b/views/event/EventWindow.cpp @@ -67,7 +67,7 @@ static QString typeDisplayName(EventType t) { EventWindow::EventWindow(QWidget *parent) : QDialog(parent) { setWindowTitle("События"); - resize(1400, 600); + resize(1400, 700); // ── Таблица ─────────────────────────────────────────────────────────────── m_table = new QTableWidget(this); @@ -91,6 +91,19 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) { m_table->setColumnWidth(COL_SENT, 45); m_table->setColumnWidth(COL_ACTION, 80); + // ── Панель скриншота ────────────────────────────────────────────────────── + m_screenshotLabel = new QLabel(this); + m_screenshotLabel->setAlignment(Qt::AlignCenter); + m_screenshotLabel->setMinimumWidth(300); + m_screenshotLabel->setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;"); + m_screenshotLabel->setText("Выберите событие\nдля просмотра скриншота"); + + auto *splitter = new QSplitter(Qt::Horizontal, this); + splitter->addWidget(m_table); + splitter->addWidget(m_screenshotLabel); + splitter->setStretchFactor(0, 3); + splitter->setStretchFactor(1, 1); + // ── Пагинация ───────────────────────────────────────────────────────────── m_prevBtn = new QPushButton("←", this); m_nextBtn = new QPushButton("→", this); @@ -108,7 +121,7 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) { paginationLayout->addWidget(refreshBtn); auto *mainLayout = new QVBoxLayout(this); - mainLayout->addWidget(m_table, 1); + mainLayout->addWidget(splitter, 1); mainLayout->addLayout(paginationLayout); // ── Сигналы ─────────────────────────────────────────────────────────────── @@ -122,6 +135,11 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) { m_page = 0; loadPage(); }); + connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) { + if (row >= 0 && row < m_eventIds.size()) { + showScreenshot(m_eventIds[row]); + } + }); connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int col) { if (col != COL_JSON) return; auto *item = m_table->item(row, col); @@ -150,9 +168,11 @@ void EventWindow::loadPage() { m_page = qBound(0, m_page, m_totalPages - 1); const QList events = EventDAO::getEvents(m_page, m_pageSize); + m_eventIds.clear(); m_table->setRowCount(static_cast(events.size())); for (int i = 0; i < events.size(); ++i) { + m_eventIds.append(events[i].id); const auto &e = events[i]; m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id))); m_table->setItem(i, COL_TIME, new QTableWidgetItem( @@ -204,6 +224,24 @@ void EventWindow::loadPage() { updatePagination(); } +void EventWindow::showScreenshot(int eventId) { + const QByteArray data = EventDAO::getScreenshot(eventId); + if (data.isEmpty()) { + m_screenshotLabel->setText("Нет скриншота"); + return; + } + + QPixmap pixmap; + pixmap.loadFromData(data); + if (pixmap.isNull()) { + m_screenshotLabel->setText("Ошибка загрузки"); + return; + } + + m_screenshotLabel->setPixmap(pixmap.scaled( + m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + void EventWindow::updatePagination() { m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages)); m_prevBtn->setEnabled(m_page > 0); diff --git a/views/event/EventWindow.h b/views/event/EventWindow.h index 06663e3..ca6fd99 100644 --- a/views/event/EventWindow.h +++ b/views/event/EventWindow.h @@ -2,6 +2,7 @@ #include #include #include +#include #include class EventWindow final : public QDialog { @@ -13,6 +14,7 @@ public: private: QTableWidget *m_table; QLabel *m_pageLabel; + QLabel *m_screenshotLabel; QPushButton *m_prevBtn; QPushButton *m_nextBtn; @@ -20,6 +22,10 @@ private: int m_pageSize = 50; int m_totalPages = 1; + // Кешируем event id текущей страницы для загрузки скриншота по клику + QList m_eventIds; + void loadPage(); void updatePagination(); + void showScreenshot(int eventId); }; diff --git a/views/log/FileLogWindow.cpp b/views/log/FileLogWindow.cpp index 336bec5..fed22b9 100644 --- a/views/log/FileLogWindow.cpp +++ b/views/log/FileLogWindow.cpp @@ -1,10 +1,21 @@ #include "FileLogWindow.h" +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include #include +#include "dao/EventDAO.h" #include "dao/GeneralLogDAO.h" +#include "dao/SettingsDAO.h" static constexpr int COL_ID = 0; static constexpr int COL_TIME = 1; @@ -38,6 +49,8 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) { m_pageLabel->setMinimumWidth(140); auto *refreshBtn = new QPushButton("Обновить", this); + m_sendBtn = new QPushButton("Отправить", this); + m_sendBtn->setStyleSheet("background-color: #0088cc; color: white; border-radius: 4px; padding: 4px 12px;"); auto *paginationLayout = new QHBoxLayout; paginationLayout->addWidget(m_prevBtn); @@ -46,6 +59,7 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) { paginationLayout->addStretch(); paginationLayout->addWidget(m_showAllCheck); paginationLayout->addWidget(refreshBtn); + paginationLayout->addWidget(m_sendBtn); auto *mainLayout = new QVBoxLayout(this); mainLayout->addWidget(m_table, 1); @@ -65,6 +79,7 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) { m_page = 0; loadPage(); }); + connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogToTelegram); loadPage(); } @@ -102,3 +117,141 @@ void FileLogWindow::updatePagination() { m_prevBtn->setEnabled(m_page > 0); m_nextBtn->setEnabled(m_page < m_totalPages - 1); } + +void FileLogWindow::sendLogToTelegram() { + const QString logPath = QCoreApplication::applicationDirPath() + "/application.log"; + if (!QFile::exists(logPath)) { + QMessageBox::warning(this, "Ошибка", "Файл application.log не найден"); + return; + } + + m_sendBtn->setEnabled(false); + m_sendBtn->setText("Подготовка..."); + + // Собираем временную папку для архива + const QString tmpDir = QDir::tempPath() + "/arc_logs_export"; + QDir().mkpath(tmpDir); + + // 1. Копируем application.log + QFile::remove(tmpDir + "/application.log"); + QFile::copy(logPath, tmpDir + "/application.log"); + + // 2. Экспортируем events с ошибками в errors.txt + const QList errors = EventDAO::getAllEvents(EventStatus::Error); + if (!errors.isEmpty()) { + QFile errFile(tmpDir + "/errors.txt"); + if (errFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream out(&errFile); + for (const EventInfo &e : errors) { + out << "--- Event #" << e.id << " ---\n" + << "Type: " << eventTypeToString(e.type) << "\n" + << "Bank: " << e.bankName << "\n" + << "Device: " << e.deviceId << "\n" + << "Amount: " << e.amount << "\n" + << "Phone: " << e.phone << "\n" + << "Comment: " << e.comment << "\n" + << "Time: " << e.timestamp.toString(Qt::ISODate) << "\n" + << "JSON: " << e.json << "\n\n"; + } + errFile.close(); + } + } + + // 3. Экспортируем скриншоты ошибок из events + QDir().mkpath(tmpDir + "/screenshots"); + int screenshotCount = 0; + for (const EventInfo &e : errors) { + const QByteArray screenshot = EventDAO::getScreenshot(e.id); + if (screenshot.isEmpty()) continue; + const QString fileName = QString("screenshots/%1_%2_%3.png") + .arg(e.id) + .arg(eventTypeToString(e.type)) + .arg(e.timestamp.toString("yyyyMMdd_HHmmss")); + QFile imgFile(tmpDir + "/" + fileName); + if (imgFile.open(QIODevice::WriteOnly)) { + imgFile.write(screenshot); + imgFile.close(); + ++screenshotCount; + } + } + + // 4. Создаём ZIP + const QString zipPath = QDir::tempPath() + "/arc_logs.zip"; + QFile::remove(zipPath); + + QProcess zip; + zip.setWorkingDirectory(tmpDir); + zip.start("zip", QStringList() << "-r" << zipPath << "."); + zip.waitForFinished(30000); + + // Чистим временную папку + QDir(tmpDir).removeRecursively(); + + if (zip.exitCode() != 0 || !QFile::exists(zipPath)) { + QMessageBox::warning(this, "Ошибка", "Не удалось создать архив"); + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + return; + } + + m_sendBtn->setText("Отправка..."); + + QFile *zipFile = new QFile(zipPath); + if (!zipFile->open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив"); + delete zipFile; + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + return; + } + + // Telegram Bot API + const QString botToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8"; + const QString chatId = "-5177086220"; + const QString desktopId = SettingsDAO::get("desktop_id"); + const QString caption = "Logs from desktop: " + desktopId + + "\nErrors: " + QString::number(errors.size()) + + "\nScreenshots: " + QString::number(screenshotCount); + + const QUrl url("https://api.telegram.org/bot" + botToken + "/sendDocument"); + + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + QHttpPart chatPart; + chatPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="chat_id")"); + chatPart.setBody(chatId.toUtf8()); + multiPart->append(chatPart); + + QHttpPart captionPart; + captionPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="caption")"); + captionPart.setBody(caption.toUtf8()); + multiPart->append(captionPart); + + QHttpPart filePart; + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, + R"(form-data; name="document"; filename="arc_logs.zip")"); + filePart.setBodyDevice(zipFile); + zipFile->setParent(multiPart); + multiPart->append(filePart); + + auto *manager = new QNetworkAccessManager(this); + QNetworkReply *reply = manager->post(QNetworkRequest(url), multiPart); + multiPart->setParent(reply); + + connect(reply, &QNetworkReply::finished, this, [this, reply, manager, zipPath]() { + reply->deleteLater(); + manager->deleteLater(); + QFile::remove(zipPath); + + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + + if (reply->error() != QNetworkReply::NoError) { + QMessageBox::warning(this, "Ошибка", "Не удалось отправить: " + reply->errorString()); + return; + } + + QMessageBox::information(this, "Готово", "Логи отправлены в Telegram"); + }); +} diff --git a/views/log/FileLogWindow.h b/views/log/FileLogWindow.h index 9a7adeb..49f58a1 100644 --- a/views/log/FileLogWindow.h +++ b/views/log/FileLogWindow.h @@ -16,6 +16,7 @@ private: QLabel *m_pageLabel; QPushButton *m_prevBtn; QPushButton *m_nextBtn; + QPushButton *m_sendBtn; QCheckBox *m_showAllCheck; int m_page = 0; @@ -25,4 +26,5 @@ private: QStringList currentLevelFilter() const; void loadPage(); void updatePagination(); + void sendLogToTelegram(); };