From 59cef50ad3e49dcec537023b25f5fa74590c2bc3 Mon Sep 17 00:00:00 2001 From: slava Date: Wed, 7 May 2025 14:15:08 +0700 Subject: [PATCH] Add enhanced account and transaction handling features Refactored code to improve account and transaction data parsing and management, introducing support for additional fields like card numbers and account statuses. Added new utilities for database connections, device-specific screenshot handling, and XML parsing for enriched account and transaction details. --- android/adb/AdbUtils.cpp | 34 +++-- android/adb/AdbUtils.h | 4 +- android/rshb/CommonScript.cpp | 230 ++++++++++++++++++++++++++++++ android/rshb/CommonScript.h | 57 ++++++++ android/rshb/HomeScreenScript.cpp | 84 ++++++++++- android/rshb/PayByPhoneScript.cpp | 55 +++++++ android/rshb/PayByPhoneScript.h | 27 ++++ android/rshb/ScreenXmlParser.cpp | 134 ++++++++++++++--- android/rshb/ScreenXmlParser.h | 3 + database/DatabaseManager.cpp | 66 +++++++-- database/DatabaseManager.h | 19 ++- database/app_data.db | Bin 2621440 -> 2621440 bytes database/dao/AccountDAO.cpp | 72 +++++++--- database/dao/AccountDAO.h | 2 + database/dao/ApplicationDAO.cpp | 94 ++++++++++++ database/dao/ApplicationDAO.h | 12 ++ database/dao/DeviceDAO.cpp | 49 +++++-- database/dao/DeviceDAO.h | 2 + main.cpp | 43 +++--- migrations.sql | 23 +-- models/db/AccountInfo.h | 55 ++++++- models/db/ApplicationInfo.h | 17 +++ models/db/DeviceInfo.h | 6 +- models/db/TransactionInfo.h | 69 ++++----- views/AccountWindow.cpp | 4 +- views/TransactionWindow.cpp | 2 +- 26 files changed, 1001 insertions(+), 162 deletions(-) create mode 100644 android/rshb/CommonScript.cpp create mode 100644 android/rshb/CommonScript.h create mode 100644 android/rshb/PayByPhoneScript.cpp create mode 100644 android/rshb/PayByPhoneScript.h create mode 100644 database/dao/ApplicationDAO.cpp create mode 100644 database/dao/ApplicationDAO.h create mode 100644 models/db/ApplicationInfo.h diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp index 38124fa..bad48ed 100644 --- a/android/adb/AdbUtils.cpp +++ b/android/adb/AdbUtils.cpp @@ -104,16 +104,18 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, const int idx) { process.start("sh", {"-c", xml}); process.waitForFinished(); - // TODO делаем скрин, убрать потом - // adb -s 6edc4a47 shell 'screencap -p /sdcard/screen.png' && adb -s 6edc4a47 pull /sdcard/screen.png screen_step_1.png && adb -s 6edc4a47 shell 'rm /sdcard/screen.png' - QProcess processScreenshot; - QString screenshot = QString( - "adb -s %1 shell 'screencap -p /sdcard/screen.png' && " - "adb -s %1 pull /sdcard/screen.png screen_step_%2.png && " - "adb -s %1 shell 'rm /sdcard/screen.png'" - ).arg(deviceId, QString::number(idx)); - processScreenshot.start("sh", {"-c", screenshot}); - processScreenshot.waitForFinished(); + if (false) { + // TODO делаем скрин, убрать потом + // adb -s 6edc4a47 shell 'screencap -p /sdcard/screen.png' && adb -s 6edc4a47 pull /sdcard/screen.png screen_step_1.png && adb -s 6edc4a47 shell 'rm /sdcard/screen.png' + QProcess processScreenshot; + QString screenshot = QString( + "adb -s %1 shell 'screencap -p /sdcard/screen.png' && " + "adb -s %1 pull /sdcard/screen.png screen_step_%2.png && " + "adb -s %1 shell 'rm /sdcard/screen.png'" + ).arg(deviceId, QString::number(idx)); + processScreenshot.start("sh", {"-c", screenshot}); + processScreenshot.waitForFinished(); + } // чтобы убедиться, что мы прочитали без ошибок if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { @@ -128,6 +130,18 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, const int idx) { return QString::fromUtf8(xmlBytes); } +bool AdbUtils::takeScreenshot(const QString &deviceId, const QString &name) { + QProcess process; + QString screenshot = QString( + "adb -s %1 shell 'screencap -p /sdcard/screen.png' && " + "adb -s %1 pull /sdcard/screen.png screen_%2.png && " + "adb -s %1 shell 'rm /sdcard/screen.png'" + ).arg(deviceId, name); + process.start("sh", {"-c", screenshot}); + process.waitForFinished(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + bool AdbUtils::makeTap(const QString &deviceId, const int x, const int y) { QProcess process; process.start("adb", { diff --git a/android/adb/AdbUtils.h b/android/adb/AdbUtils.h index 23c40de..2550cef 100644 --- a/android/adb/AdbUtils.h +++ b/android/adb/AdbUtils.h @@ -17,7 +17,9 @@ public: static bool tryToKillApplication(const QString &deviceId, const QString &packageName); - static QString getScreenDumpAsXml(const QString &deviceId, const int idx); + static QString getScreenDumpAsXml(const QString &deviceId, int idx); + + static bool takeScreenshot(const QString &deviceId, const QString &name); static bool makeTap(const QString &deviceId, int x, int y); diff --git a/android/rshb/CommonScript.cpp b/android/rshb/CommonScript.cpp new file mode 100644 index 0000000..41b2c75 --- /dev/null +++ b/android/rshb/CommonScript.cpp @@ -0,0 +1,230 @@ +#include "CommonScript.h" +#include +#include + +#include "adb/AdbUtils.h" + +CommonScript::CommonScript(QObject *parent) + : QObject(parent) { +} + +CommonScript::~CommonScript() = default; + +void CommonScript::start() { + doStart(); + emit finished(); +} + +Node CommonScript::swipeToButton( + const QString &deviceId, + const QString &text, int &counter, const int width, const int height +) { + for (int i = 0; i < 10; ++i) { + if (Node button = xmlScreenParser.findButtonNode(text, true); button.x() > 0 && button.y() > 0) { + return button; + } + const int x = width / 2; + const int offset = height / 4; + AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter)); + } + return {}; +} + +bool CommonScript::inputPinCode(const QString &deviceId, const QList &pincode) { + for (const Node &node: pincode) { + const bool completePincode = AdbUtils::makeTap(deviceId, node.x(), node.y()); + if (!completePincode) { + return false; + } + QThread::msleep(QRandomGenerator::global()->bounded(100, 901)); + } + return true; +} + +bool CommonScript::goToHomeScreen( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + const int width, + const int height +) { + // Запустить устройство + if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) { + qDebug() << "Process already running, pid: " << pid; + AdbUtils::getScreenDumpAsXml(deviceId, 1000); + AdbUtils::tryToKillApplication(deviceId, packageName); + } + if (!AdbUtils::tryToStartApplication(deviceId, packageName)) { + AdbUtils::takeScreenshot(deviceId, "run_app_error"); + qWarning() << "Process has not started"; + return false; + } + + // Даем минимальное время на запуск + QThread::msleep(4500); + int counter = 0; + int restarted = 0; + int totalCounter = 0; + bool findAndInputPincode = false; + while (true) { + counter++; + totalCounter++; + + // 3 раза уже перезапускали приложение, завершаем попытки + if (restarted > 2) { + AdbUtils::takeScreenshot(deviceId, "3_times_try_start"); + return false; + } + + // Если более 30 секунд мы пытаемся перейти на домашнюю, перезапускаем + if (counter > 30) { + AdbUtils::tryToKillApplication(deviceId, packageName); + QThread::msleep(4500); + ++restarted; + counter = 0; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, totalCounter + 1)); + + if (xmlScreenParser.isHomeScreen()) { + return true; + } + + // Проверяем на банеры после пин-кодом + Node closeBannerOrDialogNode = xmlScreenParser.tryToFindBannerOrDialog(width, height); + if (closeBannerOrDialogNode.x() != -1 && closeBannerOrDialogNode.y() != -1) { + AdbUtils::makeTap(deviceId, closeBannerOrDialogNode.x(), closeBannerOrDialogNode.y()); + QThread::msleep(1000); + continue; + } + + // Проверяем на банеры перед пин-кодом + Node closeBannerNode = xmlScreenParser.tryToFindSecurityBanner(); + if (!closeBannerNode.isEmpty()) { + AdbUtils::makeTap(deviceId, closeBannerNode.x(), closeBannerNode.y()); + QThread::msleep(1000); + continue; + } + + // Проверяем, что это экран ввода пин-кода + if (!findAndInputPincode) { + if (const QList pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); + pincode.size() == 4) { + inputPinCode(deviceId, pincode); + // После ввода пин-код нужно сделать задержку, пока подгрузка идет, чтобы снова не спарсить + findAndInputPincode = true; + QThread::msleep(3000); + continue; + } + } + + QThread::msleep(1000); + + // Если что-то пойдет не по плану... + if (totalCounter > 100) { + AdbUtils::takeScreenshot(deviceId, "too_many_attempts"); + return false; + } + } +} + +bool CommonScript::runAppAndGoToHomeScreen() { + const QString deviceId = "6edc4a47"; + const QString packageName = "ru.rshb.dbo"; + const QString pinCode = "1102"; + const int width = 720; + const int height = 1600; + + const QString cardNumber = "7 1 8 3"; + // В том числе и данные на каком устройстве это делать + // TODO тут код работы с БД + + // +7 (964) 181-51-46 + const QString phoneNumber = "+79641815146"; + const QString bankName = "Альфа-Банк"; + const QString paySum = "250"; + + xmlScreenParser.test(); + return false; + // выше достаем данные и все прочее + + return goToHomeScreen(deviceId, packageName, pinCode, width, height); +} + +bool CommonScript::tryToCloseAllBannersOnHomePage(const QString &deviceId, const int width, const int height) { + Node closeBannerOrDialogNode = xmlScreenParser.tryToFindBannerOrDialog(width, height); + if (closeBannerOrDialogNode.x() != -1 && closeBannerOrDialogNode.y() != -1) { + AdbUtils::makeTap(deviceId, closeBannerOrDialogNode.x(), closeBannerOrDialogNode.y()); + return true; + } + + // TODO тут все другие банеры попробовать закрыть + return false; +} + +bool CommonScript::goToAllProducts() { + int counter = 100; + const QString deviceId = "6edc4a47"; + const int width = 720; + const int height = 1600; + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, counter)); + if (tryToCloseAllBannersOnHomePage(deviceId, width, height)) { + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter)); + } + + if (xmlScreenParser.isHomeScreen()) { + // идем на экран "Все продукты" + if (Node allCardTitle = xmlScreenParser.findTextNode("Счета и карты"); !allCardTitle.isEmpty()) { + // 1. Если нет кнопки видимой, свайпаем. Кликаем на "Все продукты" + for (int i = 0; i < 5; ++i) { + Node allProductsBtn = xmlScreenParser.findButtonNode("Все продукты", false); + // Node allProductsBtn = xmlScreenParser.findTextNode("Курсы валют и металлов"); для теста + if (allProductsBtn.x() > 0 && allProductsBtn.y() > 0) { + qDebug() << "--- All Product in focus"; + AdbUtils::makeTap(deviceId, allProductsBtn.x(), allProductsBtn.y()); + QThread::msleep(1000); + break; + } + AdbUtils::makeSwipe(deviceId, + allCardTitle.x(), allCardTitle.y(), + allCardTitle.x(), allCardTitle.y() - 200); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter)); + } + + for (int i = 0; i < 5; ++i) { + Node allProductsTitle = xmlScreenParser.findTextNode("Все продукты"); + // Проверяем что вверху заголовок + if (!allProductsTitle.isEmpty() && allProductsTitle.y() < 300) { + return true; + } + + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter)); + } + + } else { + // пишем в БД, что не дошли до домашней страницы, и завершаемся + // сохраняем скрин приложения + // дделаем алерт + } + } else { + qDebug() << "Not home screen..."; + } + return false; +} + +bool CommonScript::readTransactionData() { + return false; +} + +// 1. топаем на домашнюю +// 2. распарсить мои карты и вернуть данные +// 3. синхронизировать историю транзакций (с домашней) + + +void CommonScript::stop() { + m_running = false; +} diff --git a/android/rshb/CommonScript.h b/android/rshb/CommonScript.h new file mode 100644 index 0000000..ded17f7 --- /dev/null +++ b/android/rshb/CommonScript.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include "ScreenXmlParser.h" + +class CommonScript : public QObject { + Q_OBJECT + +public: + explicit CommonScript(QObject *parent = nullptr); + + ~CommonScript() override; + +protected: + virtual void doStart() = 0; + + // static method + static bool inputPinCode(const QString &deviceId, const QList &pincode); + + bool goToAllProducts(); + + Node swipeToButton( + const QString &deviceId, + const QString &text, + int &counter, int width, int height + ); + + bool runAppAndGoToHomeScreen(); + + bool tryToCloseAllBannersOnHomePage( + const QString &deviceId, int width, int height + ); + + bool readTransactionData(); + + bool m_running = true; + ScreenXmlParser xmlScreenParser; + QString m_packageName; + QString m_pinCode; + int m_width = 0; + int m_height = 0; + +signals: + void finished(); + +public slots: + virtual void start() final; + + virtual void stop() final; + +private: + // Q_DISABLE_COPY(CommonScript); + + bool goToHomeScreen( + const QString &deviceId, const QString &packageName, + const QString &pinCode, int width, int height); +}; diff --git a/android/rshb/HomeScreenScript.cpp b/android/rshb/HomeScreenScript.cpp index 51e5cba..7a01f94 100644 --- a/android/rshb/HomeScreenScript.cpp +++ b/android/rshb/HomeScreenScript.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "ScreenXmlParser.h" @@ -112,6 +113,40 @@ bool HomeScreenScript::goToHomeScreen( } } +QString getPriceAsString(const double value) { + QString sign = value < 0 ? "Минус" : "Плюс"; + const auto intValue = static_cast(std::abs(value)); // округление до целого + + QString number = QString::number(intValue); + QString formatted; + + int count = 0; + for (int i = number.length() - 1; i >= 0; --i) { + formatted.prepend(number[i]); + ++count; + if (count % 3 == 0 && i != 0) { + formatted.prepend(' '); + } + } + + return QString("%1 %2").arg(sign, formatted); +} + +Node swipeToButton( + ScreenXmlParser &xmlScreenParser, const QString &deviceId, + const QString &text, int &counter, const int width, const int height +) { + for (int i = 0; i < 10; ++i) { + if (Node button = xmlScreenParser.findButtonNode(text, true); button.x() > 0 && button.y() > 0) { + return button; + } + const int x = width / 2; + const int offset = height / 4; + AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter)); + } + return {}; +} void HomeScreenScript::start() { // Достаем данные из БД по ИД транзакции, перед тем как запустить мы кладем в БД все данные @@ -130,8 +165,53 @@ void HomeScreenScript::start() { const QString bankName = "Альфа-Банк"; const QString paySum = "10"; + int index = 0; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++index)); + + // xmlScreenParser.test(); + // тут просто получаем инфу которую спарсил + // == 1. Входящий (TransactionType::Phone) + // Перевод через СБП от Егор Иванович Ж (+7(000)-000-00-87) из Озон Банк (Ozon) на 40817810328000022077 . + // Идентификатор операции в СБП С2С B5121111443333300170011491301 , Дата операции 01.05.2025 14:14:41 Статус перевода Исполнен + // == 2. Исходящий (TransactionType::Phone) + // Перевод Надежда Иванкина К** в Сбербанк через СБП, по номеру телефона +79000810046, ID перевода 445900141, дата 03.05.2025 + + // (TransactionType::Card) -> T-BANK CARD2CARD + // TransactionInfo trInfo = xmlScreenParser.parseTransactionInfoHistory(width, height); + // qDebug().noquote().nospace() << convertTransactionToString(trInfo); + + // Если начинается с ЕСПП - то не кликаем еще, ждет обработку + // А все переводы по карте - они не начинаются со слова "Перевод" -> + // .. пробуем кликать, но если не кликается ??? + QList transactions = xmlScreenParser.parseTodayAndYesterdayHistory(); + for (const TransactionInfo &info: transactions) { + QString text = QString("%1 %2").arg(info.shortDescription, getPriceAsString(info.amount)); + qDebug().noquote().nospace() << convertTransactionToString(info); + + // Node trButton = xmlScreenParser.findButtonNode(text, true); + Node trButton = swipeToButton(xmlScreenParser, deviceId, text, index, width, height); + if (!trButton.isEmpty()) { + AdbUtils::makeTap(deviceId, trButton.x(), trButton.y()); + QThread::msleep(500); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++index)); + + Node moreDetailBtn = xmlScreenParser.findButtonNode("Детали операции", false); + if (!moreDetailBtn.isEmpty()) { + AdbUtils::makeTap(deviceId, moreDetailBtn.x(), moreDetailBtn.y()); + QThread::msleep(200); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++index)); + TransactionInfo info = xmlScreenParser.parseTransactionInfoHistory(width, height); + qDebug().noquote().nospace() << convertTransactionToString(info); + } + + AdbUtils::goBack(deviceId); + } else { + qWarning() << "--- trButton not found.........."; + } + // Перевод ВЛАДИСЛАВ НИКОЛАЕВИЧ К** в Т-Банк через СБП, по номеру телефона +7985553... Минус 10 рублей 0 копеек + } - xmlScreenParser.test(); return; int payCounter = 0; @@ -294,10 +374,8 @@ void HomeScreenScript::start() { if (!paymentCompleted.isEmpty()) { // Если вдруг исполнен - } else if (!paymentInProgress.isEmpty()) { // Если все еще в ожидании - } else { qWarning() << "--- paymentCompleted or paymentInProgress not found..."; } diff --git a/android/rshb/PayByPhoneScript.cpp b/android/rshb/PayByPhoneScript.cpp new file mode 100644 index 0000000..08bea1a --- /dev/null +++ b/android/rshb/PayByPhoneScript.cpp @@ -0,0 +1,55 @@ +#include "PayByPhoneScript.h" + +#include + +#include "DatabaseManager.h" +#include "dao/AccountDAO.h" +#include "dao/ApplicationDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "db/ApplicationInfo.h" +#include "db/DeviceInfo.h" + +PayByPhoneScript::PayByPhoneScript( + AccountInfo account, + QString phone, + QString bankName, + const double amount, + QObject *parent +) : CommonScript(parent), + m_account(std::move(account)), m_phone(std::move(phone)), + m_bankName(std::move(bankName)), m_amount(amount) { +} + +PayByPhoneScript::~PayByPhoneScript() = default; + +void PayByPhoneScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); + const ApplicationInfo app = ApplicationDAO::getApplicationsByDeviceId(m_account.deviceId, m_account.appName); + + if (device.id.isEmpty() || app.id.isEmpty()) { + qDebug() << "Device or app not found...."; + return; + } + m_width = device.screenWidth; + m_height = device.screenHeight; + m_pinCode = app.pinCode; + m_packageName = app.package; + + if (runAppAndGoToHomeScreen()) { + if (goToAllProducts()) { + QList accounts = xmlScreenParser.parseAccountsInfo(); + for (AccountInfo &account: accounts) { + account.deviceId = m_account.deviceId; + account.appName = m_account.appName; + if (!AccountDAO::upsertAccount(account)) { + qDebug() << "Not updated: " << account.lastNumbers; + } + } + } else { + qDebug() << "Не смогли открыть 'Все продукты'"; + } + } else { + qDebug() << "Не смогли дойти до домашней страницы...."; + } +} diff --git a/android/rshb/PayByPhoneScript.h b/android/rshb/PayByPhoneScript.h new file mode 100644 index 0000000..eec6488 --- /dev/null +++ b/android/rshb/PayByPhoneScript.h @@ -0,0 +1,27 @@ +#pragma once + +#include "CommonScript.h" + +class PayByPhoneScript final : public CommonScript { + Q_OBJECT + +public: + PayByPhoneScript( + AccountInfo account, + QString phone, + QString bankName, + double amount, + QObject *parent = nullptr + ); + + ~PayByPhoneScript() override; + +protected: + void doStart() override; + +private: + const AccountInfo m_account; + const QString m_phone; + const QString m_bankName; + const double m_amount; +}; diff --git a/android/rshb/ScreenXmlParser.cpp b/android/rshb/ScreenXmlParser.cpp index 955c572..ebbbf56 100644 --- a/android/rshb/ScreenXmlParser.cpp +++ b/android/rshb/ScreenXmlParser.cpp @@ -12,6 +12,7 @@ #include "adb/AdbUtils.h" #include "android/xml/Node.h" +#include "db/AccountInfo.h" #include "db/TransactionInfo.h" ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { @@ -21,6 +22,7 @@ ScreenXmlParser::~ScreenXmlParser() = default; static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); static const QRegularExpression onlyDigitsRegex(R"([^0-9])"); +static const QRegularExpression onlyDigitsAndDotRegex(R"([^0-9.])"); static const QRegularExpression rePhone(R"raw((\+7\(?\d{3}\)?[- ]?\d{3}[- ]?\d{2}[- ]?\d{2}))raw"); static const QRegularExpression reFromBank(R"raw(из (.+?) на)raw"); @@ -31,6 +33,8 @@ static const QRegularExpression reDate(R"((?:Дата операции|дата) static const QRegularExpression reSender(R"(от ([^\(]+))"); static const QRegularExpression reNameAlt(R"(Перевод ([А-Яа-яЁё\s\*]+) в)"); +static const QRegularExpression reCardData(R"(цифрами\s+(\d\s\d\s\d\s\d).*?Сумма:\s([\d\s]+)\sруб)"); + Node ScreenXmlParser::tryToFindBannerOrDialog(const int width, const int height) { // 1. Ищем push banner - он перекрывает весь экран Node first = m_nodes[0]; @@ -106,7 +110,6 @@ Node ScreenXmlParser::findFirstEditText() { return {}; } - Node ScreenXmlParser::findButtonNode(const QString &text, const bool contains) { for (const Node &node: m_nodes) { if (contains) { @@ -122,7 +125,6 @@ Node ScreenXmlParser::findButtonNode(const QString &text, const bool contains) { return {}; } - bool ScreenXmlParser::isHomeScreen() { // Есть заголовок "Счета и карты" и кнопка "Все продукты" Node text; @@ -268,17 +270,25 @@ QString getMonthAsString(const int month) { } } -QString findSubInfo(const QDomElement &elem) { +QDomElement findSubElement(const QDomElement &elem, const QString &index) { if (elem.hasChildNodes()) { QDomNodeList subInfoList = elem.childNodes(); for (QDomNode subInfoNode: subInfoList) { if (!subInfoNode.isElement()) continue; QDomElement subInfo = subInfoNode.toElement(); - if (subInfo.attribute("index") == "1") { - return subInfo.attribute("text"); + if (subInfo.attribute("index") == index) { + return subInfo; } } } + return {}; +} + +QString findSubInfo(const QDomElement &elem, const QString &index) { + QDomElement subInfo = findSubElement(elem, index); + if (!subInfo.isNull()) { + return subInfo.attribute("text"); + } return ""; } @@ -347,19 +357,19 @@ void traverseNodes(const QDomNode &node) { QString index = infoElement.attribute("index"); switch (index.toInt()) { case 2: - qDebug() << "Дата операции:" << findSubInfo(infoElement); - continue; + qDebug() << "Дата операции:" << findSubInfo(infoElement, "1"); + break; case 5: - qDebug() << "Название:" << findSubInfo(infoElement); + qDebug() << "Название:" << findSubInfo(infoElement, "1"); qDebug() << "-------------------------------------"; // parseTransfer(findSubInfo(infoElement)); - continue; + break; case 7: - qDebug() << "Комиссия:" << findSubInfo(infoElement);; - continue; + qDebug() << "Комиссия:" << findSubInfo(infoElement, "1"); + break; default: - qDebug() << "Прочее:" << findSubInfo(infoElement);; - continue; + qDebug() << "Прочее:" << findSubInfo(infoElement, "1"); + break; } /** @@ -661,15 +671,15 @@ TransactionInfo parseExpandedInfoHistory(const QDomElement &root) { switch (QDomElement e = info.toElement(); e.attribute("index").toInt()) { case 2: // Дата операции - transaction.bankTime = findSubInfo(e); + transaction.bankTime = findSubInfo(e, "1"); break; case 5: - transaction.description = findSubInfo(e); + transaction.description = findSubInfo(e, "1"); parseTransferInfo(transaction.description, transaction); break; case 7: // Комиссия - transaction.fee = findSubInfo(e).remove(onlyDigitsRegex).toFloat(); + transaction.fee = findSubInfo(e, "1").remove(onlyDigitsRegex).toFloat(); break; default: // qDebug() << "Прочее:" << findSubInfo(e); @@ -705,9 +715,90 @@ TransactionInfo ScreenXmlParser::parseTransactionInfoHistory(int screenWidth, in return transaction; } +TransactionInfo parseExpandedInfo(const QDomElement &root) { + if (root.isElement()) { + QDomElement elem = root.toElement(); + if (elem.tagName() == "node" + && elem.attribute("class") == "android.widget.Button" + && elem.attribute("text") == "Детали операции" + ) { + TransactionInfo transaction; + // Все что ниже деталей операции + QDomNodeList detailInfo = elem.parentNode().childNodes(); + + for (int i = 0; i < detailInfo.count(); ++i) { + QDomNode info = detailInfo.at(i); + if (!info.isElement()) continue; + + QDomElement infoElement = info.toElement(); + QString index = infoElement.attribute("index"); + QDomElement ee; + switch (index.toInt()) { + case 3: + transaction.bankTime = findSubInfo(infoElement, "1"); + break; + case 4: + ee = findSubElement(infoElement, "0"); + ee = findSubElement(ee, "1"); + transaction.phone = findSubInfo(ee, "0").remove(onlyDigitsRegex); + break; + case 5: + transaction.name = findSubInfo(infoElement, "1"); + break; + case 6: + transaction.bankName = findSubInfo(infoElement, "1"); + break; + case 9: + transaction.fee = findSubInfo(infoElement, "1").remove(onlyDigitsAndDotRegex).toFloat(); + break; + case 10: + ee = findSubElement(infoElement, "1"); + transaction.trExternalId = findSubInfo(ee, "1"); + break; + default: + break; + } + } + return transaction; + } + } + + // Рекурсивно обходим всех детей + QDomNode child = root.firstChild(); + while (!child.isNull()) { + if (TransactionInfo info = parseExpandedInfo(child.toElement()); !info.bankTime.isEmpty()) { + return info; + } + child = child.nextSibling(); + } + + return {}; +} + +QList ScreenXmlParser::parseAccountsInfo() { + QList accounts; + for (const Node &node: m_nodes) { + if (node.text.contains("Номер карты с последними цифрами")) { + if (QRegularExpressionMatch match = reCardData.match(node.text); match.hasMatch()) { + AccountInfo info; + info.lastNumbers = match.captured(1).remove(" "); + info.amount = match.captured(2).remove(" ").toFloat(); + info.description = node.text.mid(0, node.text.indexOf("Номер карты")); + info.updateTime = QDateTime::currentDateTime(); + accounts.append(info); + } + } + } + + return accounts; +} + void ScreenXmlParser::test() { - return; - QFile file("assets/rshb/test/spb_test_send.xml"); + // TransactionInfo info2 = parseExpandedInfo(m_xml); + // qDebug().noquote().nospace() << convertTransactionToString(info2); + // return; + + QFile file("assets/rshb/all_products.xml"); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Cannot open file"; } @@ -720,6 +811,13 @@ void ScreenXmlParser::test() { qDebug() << "Start parsing XML:"; + // Номер карты с последними цифрами QDomElement root = doc.documentElement(); + parseAndSaveXml(doc.toString()); + for (const AccountInfo& info : parseAccountsInfo()) { + qDebug().noquote().nospace() << convertAccountToString(info); + } + // TransactionInfo info = parseExpandedInfo(root); + // qDebug().noquote().nospace() << convertTransactionToString(info); // ЕСПП } diff --git a/android/rshb/ScreenXmlParser.h b/android/rshb/ScreenXmlParser.h index 624b389..bf9559b 100644 --- a/android/rshb/ScreenXmlParser.h +++ b/android/rshb/ScreenXmlParser.h @@ -3,6 +3,7 @@ #include #include #include "android/xml/Node.h" +#include "db/AccountInfo.h" #include "db/TransactionInfo.h" class ScreenXmlParser final : public QObject { @@ -36,6 +37,8 @@ public: TransactionInfo parseTransactionInfoHistory(int screenWidth, int screenHeight); QList parseTodayAndYesterdayHistory(); + QList parseAccountsInfo(); + void test(); private: diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index 63ab141..b981a15 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -1,26 +1,64 @@ #include "DatabaseManager.h" +#include #include -#include +#include +#include -DatabaseManager::DatabaseManager() { - db = QSqlDatabase::addDatabase("QSQLITE"); - db.setDatabaseName(QCoreApplication::applicationDirPath() + "/database/app_data.db"); - if (!db.open()) { - qWarning() << "Ошибка открытия базы данных:" << db.lastError().text(); - } -} +DatabaseManager::DatabaseManager() = default; DatabaseManager::~DatabaseManager() { - if (db.isOpen()) { - db.close(); + QMutexLocker locker(&mutex); + + for (auto it = connectionPool.begin(); it != connectionPool.end(); ++it) { + QSqlDatabase::removeDatabase(it.key()); } + qDebug() << "Все соединения закрыты"; } -DatabaseManager& DatabaseManager::instance() { +DatabaseManager &DatabaseManager::instance() { static DatabaseManager instance; return instance; } -QSqlDatabase& DatabaseManager::database() { - return db; -} \ No newline at end of file +QSqlDatabase DatabaseManager::database() { + QMutexLocker locker(&mutex); + QString name = connectionName(); + + // Создаем новое соединение, если его ещё нет + if (!connectionPool.contains(name)) { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", name); + db.setDatabaseName(QCoreApplication::applicationDirPath() + "/database/app_data.db"); + + if (!db.open()) { + qWarning() << "Ошибка открытия базы данных:" << db.lastError().text(); + } + + connectionPool[name] = db; + + // Автоматическое удаление соединения при завершении потока + QObject::connect(QThread::currentThread(), &QThread::finished, [this, name]() { + QMutexLocker cLocker(&mutex); + if (connectionPool.contains(name)) { + connectionPool.remove(name); + QSqlDatabase::removeDatabase(name); + qDebug() << "Соединение" << name << "удалено"; + } + }); + } + + return connectionPool[name]; +} + +QString DatabaseManager::connectionName() { + return QString("connection_%1").arg(reinterpret_cast(QThread::currentThreadId())); +} + + +void DatabaseManager::closeConnection() { + QMutexLocker locker(&mutex); + if (const QString name = connectionName(); connectionPool.contains(name)) { + connectionPool.remove(name); + QSqlDatabase::removeDatabase(name); + qDebug() << "Соединение" << name << "удалено вручную"; + } +} diff --git a/database/DatabaseManager.h b/database/DatabaseManager.h index 7321c17..b95ea61 100644 --- a/database/DatabaseManager.h +++ b/database/DatabaseManager.h @@ -1,16 +1,25 @@ #pragma once #include +#include +#include +#include +#include -class DatabaseManager final : public QObject { - Q_OBJECT +class DatabaseManager { public: static DatabaseManager& instance(); - QSqlDatabase& database(); + QSqlDatabase database(); + void closeConnection(); private: DatabaseManager(); - ~DatabaseManager() override; - QSqlDatabase db; + ~DatabaseManager(); + + static QString connectionName(); + + QMutex mutex; + QSet activeConnections; + QMap connectionPool; }; \ No newline at end of file diff --git a/database/app_data.db b/database/app_data.db index 64483f7b3d3c668f9984c3eaaf5182e9dc0597b8..03cd6f8e25b5a0e1b756a6ac27141ccb5e1cfc95 100644 GIT binary patch delta 1299 zcma))&uiOe7{_1zux!baUc0sIWSv{3ncIe?sbr^d=n8dW6e#Vw;`z|RhO5Y;6^&#! zmP0SY-a2A-7+L#{?K%d7?XpmO$jaDdD=Um`?9d%{DRgD@Qb-5>1NO*HTIIGEebA%# zd7p>p`_BSd!*q72|0gwW*e>B8h=G_&@RBy~`nOCtQGd|H;IUi>?0 zNs{>A;_Fnf^9&m|k4(B%_&Jj{f+JZ!WLc{;KJV zES@*iaEdf#g}loO!L@3?l$H{Irx zFg3rAJdYM9KD{-EqnT`$S|7>SDFY?Lt}TwF#AD+NNn;H?P`zq=J)vduYJRb5_H3ZO`9x&T&VTs=9dotwoZQu5>)p zwLy1L<}&AU6mq<}eY16A#qsT$+4dX5?^;+=Eg$}h6s7-NsD}#~xal#t`3UuU(t9*V z;Rgsm=qI%waQStL$5}<097jl>v+LHZIa}0qEx+2jw!GS&30^bxT>uF2;$tuU_@^rEg6_4z_TUdx|6uEA4OwYgK8ekwmZ8ni#CCDa&&pCbGe z{|SwM#N_vlaVbtQS$_AD-x_T61{)MW0}Q|d5r6}tTZ4_5*xeEH2;oN{<{?fi5wL5I znqZRt;0#uKOo;~sAP$It1jqme5#bNR^*2LP+Sapj>L*$u;Km(b z_Z|E?x_5sm1t>ro$N*!&IPeN^0C*M10tbOZfC@|ihk;4pH6RBZ*}A`U^z^;|04Mx@ AhyVZp delta 1173 zcmbu-OH30%7zgluwB1L$-4^pb5DFo&h~)x$F!dERxU_3Rv=5-glc52% zF_BaqjTcFbH={9N#i}tLgv67iJ#gXR!Gi~*c&P^yL;SX+K(z7VCcn(?%$H=x5bgM9P>RM6I;(+UmRpjg$o6icKe0PBN}%K8FhRikW!jxswO#OxTds+t;a_a6=Qg+oHP@8D2Ku&2hw_=FfqjV9tK zt~H~x3Kl(8)S)k&IzO$*;s04l>%S^#=Tzo?8;)kxt`>4er=iR?8-5k)9Jwfok%(M6 z9Bp$X&^N}3OsxMzICL`XW(By!WO6tz#w7WEdp07DN2}A@vzwiicM1YDU}!TTos5bp zX;^MAQZg>|i`A$q?+9pxGNF2PRI?Qat*&N zQl@%L_|CR27Vb!fpU3$3{6~Hs+VPM-WKzn_*!i1sC&<62<;n=7yqnp%TRvgF+^*5R z5ExR(ljm@OCiqp1|IDxE^};(It>i2mPTKJ5x1pLT=d$G-4hTR31yBMNpbF(2t;tm4 z@Vko&VS~>SK?blu4K#oQS|G3KfF9I=`g=sspu`^+=%C>fNnm#hU;7P9gi!^xD?xRn t AccountDAO::getAccounts(const QString &deviceId, const QStrin QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_name, card_name, amount, update_time, status, description + SELECT id, device_id, app_name, numbers, last_numbers, amount, update_time, status, description FROM accounts WHERE device_id = :device_id AND app_name = :app_name )"); @@ -64,10 +65,11 @@ QList AccountDAO::getAccounts(const QString &deviceId, const QStrin acc.id = query.value("id").toInt(); acc.deviceId = query.value("device_id").toString(); acc.appName = query.value("app_name").toString(); - acc.cardName = query.value("card_name").toString(); + acc.numbers = query.value("numbers").toString(); + acc.lastNumbers = query.value("last_numbers").toString(); acc.amount = query.value("amount").toDouble(); acc.updateTime = QDateTime::fromString(query.value("update_time").toString(), Qt::ISODate); - acc.status = query.value("status").toString(); + acc.status = accountStatusFromString(query.value("status").toString()); acc.description = query.value("description").toString(); accounts.append(acc); } @@ -75,12 +77,25 @@ QList AccountDAO::getAccounts(const QString &deviceId, const QStrin return accounts; } -AccountInfo AccountDAO::getAccountById(const int accountId) { +AccountInfo parseAccount(const QSqlQuery &query) { AccountInfo acc; + acc.id = query.value("id").toInt(); + acc.deviceId = query.value("device_id").toString(); + acc.appName = query.value("app_name").toString(); + acc.lastNumbers = query.value("last_numbers").toString(); + acc.numbers = query.value("numbers").toString(); + acc.amount = query.value("amount").toDouble(); + acc.updateTime = QDateTime::fromString(query.value("update_time").toString(), Qt::ISODate); + acc.status = accountStatusFromString(query.value("status").toString()); + acc.description = query.value("description").toString(); + return acc; +} + +AccountInfo AccountDAO::getAccountById(const int accountId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_name, card_name, amount, update_time, status, description + SELECT id, device_id, app_name, numbers, last_numbers, amount, update_time, status, description FROM accounts WHERE id = :id LIMIT 1 @@ -90,19 +105,36 @@ AccountInfo AccountDAO::getAccountById(const int accountId) { if (!query.exec()) { qWarning() << "Ошибка при получении account по ID:" << query.lastError().text(); - return acc; + return {}; } if (query.next()) { - acc.id = query.value("id").toInt(); - acc.deviceId = query.value("device_id").toString(); - acc.appName = query.value("app_name").toString(); - acc.cardName = query.value("card_name").toString(); - acc.amount = query.value("amount").toDouble(); - acc.updateTime = QDateTime::fromString(query.value("update_time").toString(), Qt::ISODate); - acc.status = query.value("status").toString(); - acc.description = query.value("description").toString(); + return parseAccount(query); + } + return {}; +} + +AccountInfo AccountDAO::findAppAccount(const QString &appName, const QString &cardNumber) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + SELECT id, device_id, app_name, numbers, last_numbers, amount, update_time, status, description + FROM accounts + WHERE app_name = :app_name AND numbers = :numbers + LIMIT 1 + )"); + + query.bindValue(":app_name", appName); + query.bindValue(":numbers", cardNumber); + + if (!query.exec()) { + qWarning() << "Ошибка при получении account по ID:" << query.lastError().text(); + return {}; } - return acc; + if (query.next()) { + return parseAccount(query); + } + + return {}; } diff --git a/database/dao/AccountDAO.h b/database/dao/AccountDAO.h index 07a2e5b..ba6eefb 100644 --- a/database/dao/AccountDAO.h +++ b/database/dao/AccountDAO.h @@ -10,4 +10,6 @@ public: [[nodiscard]] static QList getAccounts(const QString &deviceId, const QString &appName); [[nodiscard]] static AccountInfo getAccountById(int accountId); + + [[nodiscard]] static AccountInfo findAppAccount(const QString &appName, const QString &cardNumber); }; diff --git a/database/dao/ApplicationDAO.cpp b/database/dao/ApplicationDAO.cpp new file mode 100644 index 0000000..d81f73e --- /dev/null +++ b/database/dao/ApplicationDAO.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include "ApplicationDAO.h" +#include "DatabaseManager.h" +#include "db/ApplicationInfo.h" + +bool ApplicationDAO::upsertApplication(const ApplicationInfo &info) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + INSERT INTO applications (id, device_id, pin_code, name, package, status, comment, image) + VALUES (:id, :device_id, :pin_code, :name, :package, :status, :comment, :image) + ON CONFLICT(device_id, name) + DO UPDATE SET + device_id = excluded.device_id, + pin_code = excluded.pin_code, + name = excluded.name, + package = excluded.package, + status = excluded.status, + comment = excluded.comment, + image = excluded.image + )"); + + query.bindValue(":id", info.id); + query.bindValue(":device_id", info.deviceId); + query.bindValue(":pin_code", info.pinCode); + query.bindValue(":name", info.name); + query.bindValue(":package", info.package); + query.bindValue(":status", info.status); + query.bindValue(":comment", info.comment); + query.bindValue(":image", info.image); + + if (!query.exec()) { + qWarning() << "Ошибка при вставке/обновлении application:" << query.lastError().text(); + return false; + } + + return true; +} + +bool ApplicationDAO::updatePinCode(const QString &appId, const QString &pinCode) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + UPDATE applications + SET pin_code = :pin_code + WHERE id = :id + )"); + + query.bindValue(":pin_code", pinCode); + query.bindValue(":id", appId); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении PIN-кода приложения:" << query.lastError().text(); + return false; + } + + return true; +} + +ApplicationInfo ApplicationDAO::getApplicationsByDeviceId(const QString &deviceId, const QString &appName) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + SELECT id, device_id, pin_code, name, package, status, comment, image + FROM applications + WHERE device_id = :device_id AND name = :name + LIMIT 1 + )"); + + query.bindValue(":device_id", deviceId); + query.bindValue(":name", appName); + + if (!query.exec()) { + qWarning() << "Ошибка при получении приложений по deviceId:" << query.lastError().text(); + return {}; + } + + while (query.next()) { + ApplicationInfo app; + app.id = query.value("id").toString(); + app.deviceId = query.value("device_id").toString(); + app.pinCode = query.value("pin_code").toString(); + app.name = query.value("name").toString(); + app.package = query.value("package").toString(); + app.status = query.value("status").toString(); + app.comment = query.value("comment").toString(); + app.image = query.value("image").toByteArray(); + return app; + } + + return {}; +} diff --git a/database/dao/ApplicationDAO.h b/database/dao/ApplicationDAO.h new file mode 100644 index 0000000..e4e6997 --- /dev/null +++ b/database/dao/ApplicationDAO.h @@ -0,0 +1,12 @@ +#pragma once + +#include "db/ApplicationInfo.h" + +class ApplicationDAO { +public: + [[nodiscard]] static bool upsertApplication(const ApplicationInfo &info); + + [[nodiscard]] static bool updatePinCode(const QString &appId, const QString &pinCode); + + [[nodiscard]] static ApplicationInfo getApplicationsByDeviceId(const QString &deviceId, const QString &appName); +}; diff --git a/database/dao/DeviceDAO.cpp b/database/dao/DeviceDAO.cpp index 59d608c..2f61f80 100644 --- a/database/dao/DeviceDAO.cpp +++ b/database/dao/DeviceDAO.cpp @@ -4,6 +4,20 @@ #include "DatabaseManager.h" #include "db/DeviceInfo.h" +DeviceInfo parseDevice(const QSqlQuery &query) { + DeviceInfo device; + device.id = query.value("id").toString(); + device.status = query.value("status").toString(); + device.name = query.value("name").toString(); + device.android = query.value("android").toString(); + device.screenWidth = query.value("screen_width").toInt(); + device.screenHeight = query.value("screen_height").toInt(); + device.battery = query.value("battery").toInt(); + device.updateTime = QDateTime::fromString(query.value("update_time").toString(), Qt::ISODate); + device.image = query.value("image").toByteArray(); + return device; +} + QList DeviceDAO::getAllDevices(const bool online) { QList devices; @@ -24,23 +38,34 @@ QList DeviceDAO::getAllDevices(const bool online) { } while (query.next()) { - DeviceInfo device; - device.id = query.value("id").toString(); - device.status = query.value("status").toString(); - device.name = query.value("name").toString(); - device.android = query.value("android").toString(); - device.screenWidth = query.value("screen_width").toInt(); - device.screenHeight = query.value("screen_height").toInt(); - device.battery = query.value("battery").toInt(); - device.updateTime = QDateTime::fromString(query.value("update_time").toString(), Qt::ISODate); - device.image = query.value("image").toByteArray(); - - devices.append(device); + devices.append(parseDevice(query)); } return devices; } +DeviceInfo DeviceDAO::getDeviceById(const QString &deviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + SELECT id, status, name, android, screen_width, screen_height, battery, update_time, image + FROM devices + WHERE id = :deviceId + )"); + query.bindValue(":deviceId", deviceId); + + if (!query.exec()) { + qWarning() << "Ошибка при получении устройства:" << query.lastError().text(); + return {}; + } + + while (query.next()) { + return parseDevice(query); + } + + return {}; +} + bool DeviceDAO::upsertDevice(const DeviceInfo &device) { QSqlQuery query(DatabaseManager::instance().database()); diff --git a/database/dao/DeviceDAO.h b/database/dao/DeviceDAO.h index 58d8bb4..2987c92 100644 --- a/database/dao/DeviceDAO.h +++ b/database/dao/DeviceDAO.h @@ -4,6 +4,8 @@ class DeviceDAO { public: + [[nodiscard]] static DeviceInfo getDeviceById(const QString &deviceId); + [[nodiscard]] static bool upsertDevice(const DeviceInfo &device); [[nodiscard]] static bool markDevicesOfflineExcept(const QStringList &onlineDeviceIds); diff --git a/main.cpp b/main.cpp index ddfe0aa..6d0526b 100644 --- a/main.cpp +++ b/main.cpp @@ -16,6 +16,7 @@ #include #include "rshb/HomeScreenScript.h" +#include "rshb/PayByPhoneScript.h" void setupWorkerAndThread(QCoreApplication &app) { auto *thread = new QThread; @@ -48,26 +49,30 @@ void setupWorkerAndThread(QCoreApplication &app) { } void setupScriptAutoPaymentAndThread(QCoreApplication &app) { - auto *thread = new QThread; - auto *scriptWorker = new HomeScreenScript(0); + const QString appName = "rshb"; + const QString cardNumber = "2200380317107183"; - // Перемещаем worker в новый поток thread. - // Это значит, что все слоты worker, вызываемые через connect(), теперь будут исполняться в этом фоновом потоке. - scriptWorker->moveToThread(thread); - // Когда поток стартует, запускаем start() - QObject::connect(thread, &QThread::started, scriptWorker, &HomeScreenScript::start); - // Когда работа (worker) завершена (emit finished()), останавливаем поток - QObject::connect(scriptWorker, &HomeScreenScript::finished, thread, &QThread::quit); - // Удаляем работу (worker) (deleteLater) - QObject::connect(scriptWorker, &HomeScreenScript::finished, scriptWorker, &QObject::deleteLater); - // Удаляем поток (deleteLater) - QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + const QString phoneNumber = "79641815146"; + const QString bankName = "Альфа-Банк"; + const double ammount = 250; - QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() { - scriptWorker->stop(); - }); + AccountInfo account = AccountDAO::findAppAccount(appName, cardNumber); + if (account.id != -1) { + auto *thread = new QThread; + auto *scriptWorker = new PayByPhoneScript(account, phoneNumber, bankName, ammount); - thread->start(); + scriptWorker->moveToThread(thread); + QObject::connect(thread, &QThread::started, scriptWorker, &PayByPhoneScript::start); + QObject::connect(scriptWorker, &PayByPhoneScript::finished, thread, &QThread::quit); + QObject::connect(scriptWorker, &PayByPhoneScript::finished, scriptWorker, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + + QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() { + scriptWorker->stop(); + }); + + thread->start(); + } } @@ -98,7 +103,7 @@ int main(int argc, char *argv[]) { // qDebug() << "Сохранившееся время:" << savedTime.toString(); // // - MainWindow window; - window.show(); + // MainWindow window; + // window.show(); return app.exec(); } diff --git a/migrations.sql b/migrations.sql index 085134f..02e83db 100644 --- a/migrations.sql +++ b/migrations.sql @@ -18,24 +18,27 @@ CREATE TABLE IF NOT EXISTS applications device_id TEXT, pin_code TEXT, name TEXT, + package TEXT, status TEXT, comment TEXT, image BLOB, - FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, + UNIQUE (device_id, name) ); CREATE TABLE IF NOT EXISTS accounts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - device_id TEXT, - app_name TEXT, - card_name TEXT, - description TEXT, - amount REAL, - update_time DATETIME DEFAULT CURRENT_TIMESTAMP, - status TEXT, + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT, + app_name TEXT, + numbers TEXT, + last_numbers TEXT, + description TEXT, + amount REAL, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP, + status TEXT, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, - UNIQUE (device_id, app_name, card_name) + UNIQUE (device_id, app_name, last_numbers) ); CREATE TABLE IF NOT EXISTS transactions diff --git a/models/db/AccountInfo.h b/models/db/AccountInfo.h index d7fcdf1..cf66d89 100644 --- a/models/db/AccountInfo.h +++ b/models/db/AccountInfo.h @@ -2,13 +2,58 @@ #include +enum class AccountStatus { + Active, + Blocked +}; + +inline QString accountStatusToString(const AccountStatus status) { + switch (status) { + case AccountStatus::Active: return "active"; + case AccountStatus::Blocked: return "blocked"; + default: return "active"; + } +} + +inline AccountStatus accountStatusFromString(const QString &str) { + if (str == "active") return AccountStatus::Active; + if (str == "blocked") return AccountStatus::Blocked; + return AccountStatus::Active; +} + struct AccountInfo { - int id; + int id = -1; QString deviceId; QString appName; - QString cardName; - double amount; + QString lastNumbers; + QString numbers; + double amount = 0.0; QDateTime updateTime; - QString status; + AccountStatus status = AccountStatus::Active; QString description; -}; \ No newline at end of file +}; + +inline QString convertAccountToString(const AccountInfo &info) { + QStringList lines; + QStringList emptyFields; + + lines << "AccountInfo {"; + + if (info.id != -1) lines << " id: " + QString::number(info.id); else emptyFields << "id"; + if (!info.deviceId.isEmpty()) lines << " deviceId: " + info.deviceId; else emptyFields << "deviceId"; + if (!info.appName.isEmpty()) lines << " appName: " + info.appName; else emptyFields << "appName"; + if (!info.numbers.isEmpty()) lines << " numbers: " + info.numbers; else emptyFields << "numbers"; + if (!info.lastNumbers.isEmpty()) lines << " lastNumbers: " + info.lastNumbers; else emptyFields << "lastNumbers"; + if (info.amount != 0.0) lines << " amount: " + QString::number(info.amount, 'f', 2); else emptyFields << "amount"; + if (info.updateTime.isValid()) lines << " updateTime: " + info.updateTime.toString(Qt::ISODate); else emptyFields << "updateTime"; + if (info.status != AccountStatus::Active) lines << " status: " + accountStatusToString(info.status); else emptyFields << "status"; + if (!info.description.isEmpty()) lines << " description: " + info.description; else emptyFields << "description"; + + if (!emptyFields.isEmpty()) { + lines << " --- empty: " + emptyFields.join(", "); + } + + lines << "}"; + + return lines.join('\n'); +} diff --git a/models/db/ApplicationInfo.h b/models/db/ApplicationInfo.h new file mode 100644 index 0000000..c545f26 --- /dev/null +++ b/models/db/ApplicationInfo.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +struct ApplicationInfo { + QString id; + QString deviceId; + QString pinCode; + QString name; + QString package; + QString status; + QString comment; + QByteArray image; + QDateTime updateTime; +}; diff --git a/models/db/DeviceInfo.h b/models/db/DeviceInfo.h index b87ed5c..d63b5f0 100644 --- a/models/db/DeviceInfo.h +++ b/models/db/DeviceInfo.h @@ -7,9 +7,9 @@ struct DeviceInfo { QString status; QString name; QString android; - int screenWidth; - int screenHeight; - int battery; + int screenWidth = 0; + int screenHeight = 0; + int battery = 0; QDateTime updateTime; QByteArray image; }; \ No newline at end of file diff --git a/models/db/TransactionInfo.h b/models/db/TransactionInfo.h index 81996bc..35451e2 100644 --- a/models/db/TransactionInfo.h +++ b/models/db/TransactionInfo.h @@ -71,43 +71,34 @@ struct TransactionInfo { }; inline QString convertTransactionToString(const TransactionInfo &tx) { - return QString( - "TransactionInfo {\n" - " id: %1\n" - " accountId: %2\n" - " amount: %3\n" - " fee: %4\n" - " status: %5\n" - " type: %6\n" - " phone: %7\n" - " shortDescription: %8\n" - " updatedShortDescription: %9\n" - " description: %10\n" - " updatedDescription: %11\n" - " bankName: %12\n" - " bankTime: %13\n" - " name: %14\n" - " trExternalId: %15\n" - " updateTime: %16\n" - " completeTime: %17\n" - " timestamp: %18\n" - "}" - ).arg(tx.id) - .arg(tx.accountId) - .arg(tx.amount) - .arg(tx.fee) - .arg(transactionStatusToString(tx.status)) - .arg(transactionTypeToString(tx.type)) - .arg(tx.phone) - .arg(tx.shortDescription) - .arg(tx.updatedShortDescription) - .arg(tx.description) - .arg(tx.updatedDescription) - .arg(tx.bankName) - .arg(tx.bankTime) - .arg(tx.name) - .arg(tx.trExternalId) - .arg(tx.updateTime.toString(Qt::ISODate)) - .arg(tx.completeTime.toString(Qt::ISODate)) - .arg(tx.timestamp.toString(Qt::ISODate)); + QStringList lines; + QStringList emptyFields; + + lines << "TransactionInfo {"; + + if (tx.id != -1) lines << " id: " + QString::number(tx.id); else emptyFields << "id"; + if (tx.accountId != -1) lines << " accountId: " + QString::number(tx.accountId); else emptyFields << "accountId"; + if (tx.amount != 0.0) lines << " amount: " + QString::number(tx.amount); else emptyFields << "amount"; + if (tx.fee != 0.0) lines << " fee: " + QString::number(tx.fee); else emptyFields << "fee"; + if (tx.status != TransactionStatus::Unknown) lines << " status: " + transactionStatusToString(tx.status); else emptyFields << "status"; + if (tx.type != TransactionType::Unknown) lines << " type: " + transactionTypeToString(tx.type); else emptyFields << "type"; + if (!tx.phone.isEmpty()) lines << " phone: " + tx.phone; else emptyFields << "phone"; + if (!tx.shortDescription.isEmpty()) lines << " shortDescription: " + tx.shortDescription; else emptyFields << "shortDescription"; + if (!tx.updatedShortDescription.isEmpty()) lines << " updatedShortDescription: " + tx.updatedShortDescription; else emptyFields << "updatedShortDescription"; + if (!tx.description.isEmpty()) lines << " description: " + tx.description; else emptyFields << "description"; + if (!tx.updatedDescription.isEmpty()) lines << " updatedDescription: " + tx.updatedDescription; else emptyFields << "updatedDescription"; + if (!tx.bankName.isEmpty()) lines << " bankName: " + tx.bankName; else emptyFields << "bankName"; + if (!tx.bankTime.isEmpty()) lines << " bankTime: " + tx.bankTime; else emptyFields << "bankTime"; + if (!tx.name.isEmpty()) lines << " name: " + tx.name; else emptyFields << "name"; + if (!tx.trExternalId.isEmpty()) lines << " trExternalId: " + tx.trExternalId; else emptyFields << "trExternalId"; + if (tx.updateTime.isValid()) lines << " updateTime: " + tx.updateTime.toString(Qt::ISODate); else emptyFields << "updateTime"; + if (tx.completeTime.isValid()) lines << " completeTime: " + tx.completeTime.toString(Qt::ISODate); else emptyFields << "completeTime"; + if (tx.timestamp.isValid()) lines << " timestamp: " + tx.timestamp.toString(Qt::ISODate); else emptyFields << "timestamp"; + + if (!emptyFields.isEmpty()) { + lines << " --- empty: " + emptyFields.join(", "); + } + lines << "}"; + + return lines.join('\n'); } diff --git a/views/AccountWindow.cpp b/views/AccountWindow.cpp index 0a64189..94f3182 100644 --- a/views/AccountWindow.cpp +++ b/views/AccountWindow.cpp @@ -55,13 +55,13 @@ AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) { for (int k = 0; k < accounts.size(); ++k) { AccountInfo account = accounts[k]; - QTableWidgetItem *item = new QTableWidgetItem("*" + account.cardName); + 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(account.status)); + tableWidget->setItem(k, 4, new QTableWidgetItem(accountStatusToString(account.status))); totalHeight += tableWidget->rowHeight(k); } tableWidget->setFixedHeight(totalHeight + 2); diff --git a/views/TransactionWindow.cpp b/views/TransactionWindow.cpp index a5e3766..6cdf058 100644 --- a/views/TransactionWindow.cpp +++ b/views/TransactionWindow.cpp @@ -14,7 +14,7 @@ TransactionWindow::TransactionWindow(const int accountId, QWidget *parent): QDia AccountInfo account = AccountDAO::getAccountById(accountId); - QLabel *label = new QLabel(QString("*%1 | %2").arg(account.cardName, account.description), this); + QLabel *label = new QLabel(QString("*%1 | %2").arg(account.lastNumbers, account.description), this); layout->addWidget(label); setModal(true);