From 7bf49ba749c8c6eb1f19d5caf61cfa2b51a59416 Mon Sep 17 00:00:00 2001 From: slava Date: Mon, 18 May 2026 18:42:59 +0500 Subject: [PATCH] Add `app_version` tracking: database schema migration, ADB utility support, and UI enhancements. --- android/adb/AdbUtils.cpp | 15 ++++ android/adb/AdbUtils.h | 3 + .../dushanbe/GetLastTransactionsScript.cpp | 26 +++--- config.ini | 3 + database/DatabaseManager.cpp | 6 ++ database/dao/BankProfileDAO.cpp | 27 ++++++- database/dao/BankProfileDAO.h | 2 + images/copy.png | Bin 0 -> 6963 bytes migrations.sql | 2 + models/db/BankProfileInfo.h | 2 + services/DeviceScreener.cpp | 16 ++++ views/bank/AccountSettingsWindow.cpp | 75 ++++++++++++++++-- views/bank/AccountSettingsWindow.h | 1 + views/device/DeviceSettingsWindow.cpp | 24 +++--- 14 files changed, 170 insertions(+), 32 deletions(-) create mode 100644 images/copy.png diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp index 2887a8e..f8063c3 100644 --- a/android/adb/AdbUtils.cpp +++ b/android/adb/AdbUtils.cpp @@ -740,6 +740,21 @@ QString AdbUtils::getDeviceTimezone(const QString &deviceId) { return out.trimmed(); } +QString AdbUtils::getAppVersion(const QString &deviceId, const QString &packageName) { + if (deviceId.isEmpty() || packageName.isEmpty()) { + return {}; + } + QString out; + if (!startProcess(adbPath(), + {"-s", deviceId, "shell", "dumpsys", "package", packageName}, + &out)) { + return {}; + } + static const QRegularExpression re(R"(versionName=([^\s]+))"); + const QRegularExpressionMatch m = re.match(out); + return m.hasMatch() ? m.captured(1).trimmed() : QString(); +} + void AdbUtils::unlockScreen(const QString &deviceId, const int screenWidth, const int screenHeight) { const QString adb = adbPath(); diff --git a/android/adb/AdbUtils.h b/android/adb/AdbUtils.h index c8a5ad1..154ff6f 100644 --- a/android/adb/AdbUtils.h +++ b/android/adb/AdbUtils.h @@ -56,6 +56,9 @@ public: // Возвращает таймзону устройства (напр. "Asia/Bangkok"), либо пустую строку при ошибке static QString getDeviceTimezone(const QString &deviceId); + // Возвращает versionName установленного пакета (напр. "19.17.0"), либо пустую строку при ошибке/отсутствии + static QString getAppVersion(const QString &deviceId, const QString &packageName); + private: static QString adbDir(); static bool startProcess(const QString &program, const QStringList &args, diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp index 92bc82f..560ac32 100644 --- a/android/dushanbe/GetLastTransactionsScript.cpp +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -598,7 +598,7 @@ void GetLastTransactionsScript::doStart() { } } - // Проверка времени по HH:MM:SS (±1 час, с учётом wraparound полуночи) + // Проверка времени по HH:MM:SS (±10 минут, с учётом wraparound полуночи) if (m_searchTime.isValid() && !tx.time.isEmpty()) { const QTime listTime = QTime::fromString(tx.time, "HH:mm:ss"); const QTime searchLocalTime = m_searchTime.toTimeZone(tjTz).time(); @@ -642,26 +642,28 @@ void GetLastTransactionsScript::doStart() { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); for (const Node &n : xmlScreenParser.findAllNodes()) { - if (!n.contentDesc.contains(QString::fromUtf8("Успешный платеж"))) continue; - if (!n.contentDesc.contains(QString::fromUtf8("Дата:"))) continue; + // Принимаем оба формата детальной страницы: + // — "Успешный платеж" (исходящий) с ключами "Дата:" / "Время:" + // — "Поступление" (входящий) с ключами "Дата" / "Время" + // Гейт — наличие даты И времени в любой форме. + if (!n.contentDesc.contains(QString::fromUtf8("Дата"))) continue; + if (!n.contentDesc.contains(QString::fromUtf8("Время"))) continue; const QStringList lines = n.contentDesc.split('\n'); for (int i = 0; i < lines.size(); ++i) { - const QString key = lines[i].trimmed(); + QString key = lines[i].trimmed(); + if (key.endsWith(':')) key.chop(1); // нормализуем "Дата:" → "Дата" if (i + 1 >= lines.size()) continue; const QString val = lines[i + 1].trimmed(); - if (key == QString::fromUtf8("Получатель:")) { + if (key == QString::fromUtf8("Получатель")) { detail.receiverAccount = val; - } else if (key == QString::fromUtf8("Сумма:")) { + } else if (key == QString::fromUtf8("Сумма")) { detail.amount = val.toDouble(); - } else if (key == QString::fromUtf8("Дата:")) { + } else if (key == QString::fromUtf8("Дата")) { detail.date = val; - } else if (key == QString::fromUtf8("Время:")) { + } else if (key == QString::fromUtf8("Время")) { detail.time = val; } } - if (n.contentDesc.contains(QString::fromUtf8("Успешный"))) { - detail.status = QString::fromUtf8("Успешный"); - } detailLoaded = true; break; } @@ -683,7 +685,7 @@ void GetLastTransactionsScript::doStart() { << "date=" << detail.date << "time=" << detail.time << "amount=" << detail.amount << "receiver=" << detail.receiverAccount; - // Проверяем время ±1 час + // Проверяем время ±10 минут if (m_searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) { QDateTime txTime = QDateTime::fromString( detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss"); diff --git a/config.ini b/config.ini index ef93d27..4123663 100644 --- a/config.ini +++ b/config.ini @@ -27,6 +27,7 @@ currency=ARS country=argentina name=Black phone_code=54 +app_version=0 [ozon] android_package_name=ru.ozon.fintech.finance @@ -34,6 +35,7 @@ currency=RUB country=russia name=Ozon phone_code=7 +app_version=19.17.0 [dushanbe_city_bank] android_package_name=tj.dc.next1 @@ -41,3 +43,4 @@ currency=TJS country=tajikistan name=Dushanbe City phone_code=992 +app_version=0 diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index 86ab677..18c975d 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -119,10 +119,16 @@ void DatabaseManager::initSchema() { bank_profile_id TEXT DEFAULT '', currency TEXT DEFAULT '', amount REAL DEFAULT 0, + app_version TEXT DEFAULT '', + app_version_checked_at DATETIME, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, UNIQUE (device_id, code) ))"); + // Миграция: добавляем колонки app_version и app_version_checked_at если их нет (ошибка = уже существует) + q.exec("ALTER TABLE bank_profiles ADD COLUMN app_version TEXT DEFAULT ''"); + q.exec("ALTER TABLE bank_profiles ADD COLUMN app_version_checked_at DATETIME"); + q.exec(R"(CREATE TABLE IF NOT EXISTS materials ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT, diff --git a/database/dao/BankProfileDAO.cpp b/database/dao/BankProfileDAO.cpp index 2063b80..075d775 100644 --- a/database/dao/BankProfileDAO.cpp +++ b/database/dao/BankProfileDAO.cpp @@ -151,6 +151,8 @@ BankProfileInfo parseApplication(const QSqlQuery &query) { app.bankProfileId = query.value("bank_profile_id").toString(); app.currency = query.value("currency").toString(); app.amount = query.value("amount").toDouble(); + app.appVersion = query.value("app_version").toString(); + app.appVersionCheckedAt = query.value("app_version_checked_at").toDateTime(); return app; } @@ -158,7 +160,7 @@ BankProfileInfo BankProfileDAO::getApplication(const QString &deviceId, const QS QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at FROM bank_profiles WHERE device_id = :device_id AND code = :code LIMIT 1 @@ -183,7 +185,7 @@ QList BankProfileDAO::getApplicationsByDeviceId(const QString & 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 + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at FROM bank_profiles WHERE device_id = :device_id AND install = 'yes' )"); @@ -206,7 +208,7 @@ QList BankProfileDAO::getAllApplicationsByDeviceId(const QStrin 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 + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at FROM bank_profiles WHERE device_id = :device_id )"); @@ -262,6 +264,23 @@ bool BankProfileDAO::updateAmount(const int appId, const double amount) { return true; } +bool BankProfileDAO::updateAppVersion(const int appId, const QString &appVersion) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE bank_profiles + SET app_version = :app_version, app_version_checked_at = :checked_at + WHERE id = :id + )"); + query.bindValue(":app_version", appVersion); + query.bindValue(":checked_at", QDateTime::currentDateTimeUtc()); + query.bindValue(":id", appId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении app_version:" << query.lastError().text(); + return false; + } + return true; +} + bool BankProfileDAO::updateBankProfileId(const int appId, const QString &bankProfileId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare("UPDATE bank_profiles SET bank_profile_id = :bank_profile_id WHERE id = :id"); @@ -278,7 +297,7 @@ QList BankProfileDAO::findAllByBankProfileId(const QString &ban 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 + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at FROM bank_profiles WHERE bank_profile_id = :bank_profile_id )"); diff --git a/database/dao/BankProfileDAO.h b/database/dao/BankProfileDAO.h index 7d98d5a..e0b8d53 100644 --- a/database/dao/BankProfileDAO.h +++ b/database/dao/BankProfileDAO.h @@ -36,6 +36,8 @@ public: [[nodiscard]] static bool updateAmount(int appId, double amount); + [[nodiscard]] static bool updateAppVersion(int appId, const QString &appVersion); + [[nodiscard]] static BankProfileInfo getApplication(const QString &deviceId, const QString &appCode); [[nodiscard]] static QList getApplicationsByDeviceId(const QString &deviceId); diff --git a/images/copy.png b/images/copy.png new file mode 100644 index 0000000000000000000000000000000000000000..50afd5e6202e000e57c48106c0d03ab1328fb612 GIT binary patch literal 6963 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 literal 0 HcmV?d00001 diff --git a/migrations.sql b/migrations.sql index 60aa02f..a6019ab 100644 --- a/migrations.sql +++ b/migrations.sql @@ -36,6 +36,8 @@ CREATE TABLE IF NOT EXISTS bank_profiles bank_profile_id TEXT DEFAULT '', currency TEXT DEFAULT '', amount REAL DEFAULT 0, + app_version TEXT DEFAULT '', + app_version_checked_at DATETIME, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, UNIQUE (device_id, code) ); diff --git a/models/db/BankProfileInfo.h b/models/db/BankProfileInfo.h index 0304f5b..148fac8 100644 --- a/models/db/BankProfileInfo.h +++ b/models/db/BankProfileInfo.h @@ -21,5 +21,7 @@ struct BankProfileInfo { QString bankProfileId; QString currency; double amount = 0.0; + QString appVersion; + QDateTime appVersionCheckedAt; QDateTime updateTime; }; diff --git a/services/DeviceScreener.cpp b/services/DeviceScreener.cpp index ba8a707..bbbbb2b 100644 --- a/services/DeviceScreener.cpp +++ b/services/DeviceScreener.cpp @@ -378,6 +378,22 @@ void DeviceScreener::start() { if (!BankProfileDAO::checkInstalledApps(device.id, apps)) { qWarning() << "Cant check installed apps"; } + + // Обновляем версии установленных банков (не чаще раза в 6 часов на профиль) + constexpr qint64 kAppVersionRefreshSecs = 6 * 60 * 60; + const QDateTime nowUtc = QDateTime::currentDateTimeUtc(); + for (const BankProfileInfo &profile : BankProfileDAO::getApplicationsByDeviceId(device.id)) { + if (profile.package.isEmpty()) continue; + const bool stale = !profile.appVersionCheckedAt.isValid() + || profile.appVersionCheckedAt.secsTo(nowUtc) >= kAppVersionRefreshSecs; + if (!stale) continue; + + const QString ver = AdbUtils::getAppVersion(adbSerial, profile.package); + if (ver.isEmpty()) continue; // не затираем известную версию пустотой + if (!BankProfileDAO::updateAppVersion(profile.id, ver)) { + qWarning() << "Cant update app_version for" << profile.code << profile.package; + } + } } // Синхронизация с API diff --git a/views/bank/AccountSettingsWindow.cpp b/views/bank/AccountSettingsWindow.cpp index 71c48ff..52b61fc 100644 --- a/views/bank/AccountSettingsWindow.cpp +++ b/views/bank/AccountSettingsWindow.cpp @@ -9,6 +9,10 @@ #include #include #include +#include +#include +#include +#include #include "ozon/LoginAndCheckAccountsScript.h" #include "ozon/GetProfileInfoScript.h" @@ -81,10 +85,28 @@ AccountSettingsWindow::AccountSettingsWindow( layout->addLayout(pinRow); // ── Профиль ──────────────────────────────────────────────────────── + auto *profileRow = new QHBoxLayout; m_profileLabel = new QLabel; m_profileLabel->setTextFormat(Qt::RichText); m_profileLabel->setStyleSheet("margin: 4px 0;"); - layout->addWidget(m_profileLabel); + profileRow->addWidget(m_profileLabel); + + m_copyPhoneBtn = new QPushButton; + m_copyPhoneBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png")); + m_copyPhoneBtn->setIconSize(QSize(16, 16)); + m_copyPhoneBtn->setToolTip("Копировать телефон"); + m_copyPhoneBtn->setFixedSize(28, 24); + m_copyPhoneBtn->setStyleSheet("padding: 2px;"); + m_copyPhoneBtn->hide(); + connect(m_copyPhoneBtn, &QPushButton::clicked, this, [this]() { + if (m_app.phone.isEmpty()) return; + QGuiApplication::clipboard()->setText(m_app.phone); + QToolTip::showText(m_copyPhoneBtn->mapToGlobal(QPoint(0, m_copyPhoneBtn->height())), + "Скопировано", m_copyPhoneBtn, {}, 1500); + }); + profileRow->addWidget(m_copyPhoneBtn); + profileRow->addStretch(); + layout->addLayout(profileRow); // ── Кнопки обновления ────────────────────────────────────────────── auto *actionsRow = new QHBoxLayout; @@ -197,6 +219,7 @@ void AccountSettingsWindow::updateStatusUI() { : " ● Синхронизирован"; m_profileLabel->setText(text + syncText); } + m_copyPhoneBtn->setVisible(!m_app.phone.isEmpty()); } // ── Toggle On/Off ────────────────────────────────────────────────────────── @@ -571,9 +594,9 @@ void AccountSettingsWindow::reloadCards() { m_cardsTable->clearContents(); m_cardsTable->setRowCount(accounts.size()); - m_cardsTable->setColumnCount(7); + m_cardsTable->setColumnCount(8); m_cardsTable->setHorizontalHeaderLabels({ - "Сервер", "Статус", "Время обновления", "Номер карты", "Сумма", "Валюта", "" + "Сервер", "Статус", "Время обновления", "Номер карты", "Сумма", "Валюта", "", "" }); m_cardsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); @@ -598,11 +621,32 @@ void AccountSettingsWindow::reloadCards() { timeItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_cardsTable->setItem(k, 2, timeItem); - // Номер карты + // Номер карты (текст + иконка копирования) const QString displayNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber; - auto *numItem = new QTableWidgetItem(displayNumber); - numItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); - m_cardsTable->setItem(k, 3, numItem); + const QString fullCardNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber; + auto *cardCell = new QWidget(m_cardsTable); + auto *cardLayout = new QHBoxLayout(cardCell); + cardLayout->setContentsMargins(4, 0, 4, 0); + cardLayout->setSpacing(6); + auto *cardLabel = new QLabel(displayNumber, cardCell); + cardLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + cardLayout->addWidget(cardLabel); + if (!fullCardNumber.isEmpty()) { + auto *copyCardBtn = new QPushButton(cardCell); + copyCardBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png")); + copyCardBtn->setIconSize(QSize(14, 14)); + copyCardBtn->setToolTip("Копировать номер карты"); + copyCardBtn->setFixedSize(22, 20); + copyCardBtn->setStyleSheet("padding: 1px; border: none;"); + connect(copyCardBtn, &QPushButton::clicked, this, [copyCardBtn, fullCardNumber]() { + QGuiApplication::clipboard()->setText(fullCardNumber); + QToolTip::showText(copyCardBtn->mapToGlobal(QPoint(0, copyCardBtn->height())), + "Скопировано", copyCardBtn, {}, 1500); + }); + cardLayout->addWidget(copyCardBtn); + } + cardLayout->addStretch(); + m_cardsTable->setCellWidget(k, 3, cardCell); // Сумма auto *amountItem = new QTableWidgetItem(QString::number(acc.amount)); @@ -624,6 +668,23 @@ void AccountSettingsWindow::reloadCards() { toggleMaterialStatus(acc); }); } + + // Кнопка копирования номера счёта + if (!acc.accountNumber.isEmpty()) { + auto *copyAcctBtn = new QPushButton(m_cardsTable); + copyAcctBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png")); + copyAcctBtn->setIconSize(QSize(16, 16)); + copyAcctBtn->setToolTip("Копировать номер счёта"); + copyAcctBtn->setFixedSize(28, 24); + copyAcctBtn->setStyleSheet("padding: 2px;"); + m_cardsTable->setCellWidget(k, 7, copyAcctBtn); + + connect(copyAcctBtn, &QPushButton::clicked, this, [this, copyAcctBtn, acc]() { + QGuiApplication::clipboard()->setText(acc.accountNumber); + QToolTip::showText(copyAcctBtn->mapToGlobal(QPoint(0, copyAcctBtn->height())), + "Скопировано", copyAcctBtn, {}, 1500); + }); + } } m_cardsTable->resizeColumnsToContents(); diff --git a/views/bank/AccountSettingsWindow.h b/views/bank/AccountSettingsWindow.h index 015dd9a..d292cee 100644 --- a/views/bank/AccountSettingsWindow.h +++ b/views/bank/AccountSettingsWindow.h @@ -24,6 +24,7 @@ private: QLabel *m_statusLabel; QLabel *m_pinLabel; QLabel *m_profileLabel; + QPushButton *m_copyPhoneBtn = nullptr; QPushButton *m_toggleBtn; QPushButton *m_profileBtn; QPushButton *m_cardsBtn; diff --git a/views/device/DeviceSettingsWindow.cpp b/views/device/DeviceSettingsWindow.cpp index c01552d..5df5f20 100644 --- a/views/device/DeviceSettingsWindow.cpp +++ b/views/device/DeviceSettingsWindow.cpp @@ -110,13 +110,13 @@ void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) { tableWidget->clearContents(); tableWidget->setRowCount(apps.size()); - tableWidget->setColumnCount(7); - tableWidget->setHorizontalHeaderLabels({"", "Приложение", "Пакет", "Валюта", "Статус", "Комментарий", ""}); + tableWidget->setColumnCount(8); + tableWidget->setHorizontalHeaderLabels({"", "Приложение", "Пакет", "Валюта", "Статус", "Версия на телефоне", "Версия ARC", ""}); 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); - tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch); + tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); for (int k = 0; k < apps.size(); ++k) { const BankProfileInfo &app = apps[k]; @@ -153,16 +153,22 @@ void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) { statusItem->setForeground(QBrush(Qt::darkGreen)); tableWidget->setItem(k, 4, statusItem); - // Комментарий - auto *commentItem = new QTableWidgetItem(app.comment); - commentItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); - tableWidget->setItem(k, 5, commentItem); + // Версия на телефоне (из БД, обновляется через adb) + auto *deviceVersionItem = new QTableWidgetItem(app.appVersion); + deviceVersionItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + tableWidget->setItem(k, 5, deviceVersionItem); + + // Версия ARC (из config.ini) + const QString arcVersion = configSettings.value(app.code + "/app_version").toString(); + auto *arcVersionItem = new QTableWidgetItem(arcVersion); + arcVersionItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + tableWidget->setItem(k, 6, arcVersionItem); // Кнопка "Настроить" if (app.install) { auto *settingsBtn = new QPushButton("Настроить", tableWidget); settingsBtn->setStyleSheet("background-color: green; color: white; border-radius: 4px; height: 26px;"); - tableWidget->setCellWidget(k, 6, settingsBtn); + tableWidget->setCellWidget(k, 7, settingsBtn); connect(settingsBtn, &QPushButton::clicked, this, [this, app, tableWidget]() { const BankProfileInfo dbApp = BankProfileDAO::getApplication(m_deviceId, app.code);