From 0a460d10a0295df2b5ab30471730fe6ec91fb98b Mon Sep 17 00:00:00 2001 From: slava Date: Thu, 23 Apr 2026 20:44:44 +0700 Subject: [PATCH] Add exclusive material activation logic to `MaterialDAO` and improve card selection in `BankSetupWizard`. --- android/ozon/GetCardInfoScript.cpp | 45 +++++++++++---- database/dao/MaterialDAO.cpp | 52 +++++++++++++++++ database/dao/MaterialDAO.h | 5 ++ main.cpp | 2 +- views/bank/AccountSettingsWindow.cpp | 52 +++++++++++++++-- views/bank/BankSetupWizard.cpp | 86 +++++++++++++++++++++++++--- views/bank/BankSetupWizard.h | 5 ++ 7 files changed, 222 insertions(+), 25 deletions(-) diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp index a50ee07..2b93bbc 100644 --- a/android/ozon/GetCardInfoScript.cpp +++ b/android/ozon/GetCardInfoScript.cpp @@ -251,29 +251,50 @@ void GetCardInfoScript::doStart() { AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); - // Ждём экран карты — ищем кнопку "Показать" + // Ждём экран карты и стабильной геометрии блока реквизитов. + // Без этого тап по "Показать" во время reflow уезжает ниже — в "Защиту от мошенников". + // Стабильность: "Показать" совпадает по y1 с "Реквизиты карты" И координаты не меняются в двух подряд дампах. Node showBtn; + int prevShowY = -1; for (int attempt = 0; attempt < 10; ++attempt) { - QThread::msleep(2000); + QThread::msleep(attempt == 0 ? 2000 : 500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - showBtn = xmlScreenParser.findButtonNode("Показать", false); - if (!showBtn.isEmpty()) break; + Node candidate = xmlScreenParser.findButtonNode("Показать", false); + Node requisites = xmlScreenParser.findTextNode("Реквизиты карты"); - // Если всё ещё на домашнем экране — тап не сработал, повторяем - if (xmlScreenParser.isHomeScreen()) { - cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers); - if (!cardBtn.isEmpty()) { - qDebug() << "[GetCardInfo] Still on home screen, re-tapping card" << card.lastNumbers; - AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); + if (candidate.isEmpty() || requisites.isEmpty()) { + // Если всё ещё на домашнем экране — тап по карточке не сработал, повторяем + if (xmlScreenParser.isHomeScreen()) { + cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers); + if (!cardBtn.isEmpty()) { + qDebug() << "[GetCardInfo] Still on home screen, re-tapping card" << card.lastNumbers; + AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); + } } + prevShowY = -1; + qDebug() << "[GetCardInfo] Requisites not ready, attempt" << attempt + 1 << "/ 10"; + continue; } - qDebug() << "[GetCardInfo] Card screen not loaded, attempt" << attempt + 1 << "/ 10"; + // "Показать" должен быть в одной строке с "Реквизиты карты" + if (qAbs(candidate.y1() - requisites.y1()) > 10) { + prevShowY = -1; + qDebug() << "[GetCardInfo] 'Показать' not aligned with 'Реквизиты карты', waiting reflow"; + continue; + } + + // Y должна совпасть в двух подряд дампах (layout устаканился) + if (prevShowY >= 0 && qAbs(candidate.y() - prevShowY) <= 5) { + showBtn = candidate; + break; + } + prevShowY = candidate.y(); + qDebug() << "[GetCardInfo] 'Показать' seen at y=" << candidate.y() << ", waiting for stable frame"; } if (showBtn.isEmpty()) { - qWarning() << "[GetCardInfo] 'Показать' not found for card" << card.lastNumbers; + qWarning() << "[GetCardInfo] 'Показать' not stable for card" << card.lastNumbers; AdbUtils::goBack(m_deviceId); QThread::msleep(1500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/database/dao/MaterialDAO.cpp b/database/dao/MaterialDAO.cpp index af7097e..074c62a 100644 --- a/database/dao/MaterialDAO.cpp +++ b/database/dao/MaterialDAO.cpp @@ -142,6 +142,58 @@ MaterialInfo parseAccount(const QSqlQuery &query) { return acc; } +QList MaterialDAO::activateExclusive( + const QString &deviceId, const QString &appCode, const int targetId) { + QList deactivated; + QSqlDatabase db = DatabaseManager::instance().database(); + + db.transaction(); + + QSqlQuery sel(db); + sel.prepare(R"( + SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency + FROM materials + WHERE device_id = :device_id AND app_code = :app_code + AND status = 'active' AND id != :target_id + )"); + sel.bindValue(":device_id", deviceId); + sel.bindValue(":app_code", appCode); + sel.bindValue(":target_id", targetId); + if (sel.exec()) { + while (sel.next()) deactivated.append(parseAccount(sel)); + } else { + qWarning() << "[MaterialDAO] activateExclusive select failed:" << sel.lastError().text(); + db.rollback(); + return {}; + } + + QSqlQuery off(db); + off.prepare(R"( + UPDATE materials SET status = 'off' + WHERE device_id = :device_id AND app_code = :app_code AND id != :target_id + )"); + off.bindValue(":device_id", deviceId); + off.bindValue(":app_code", appCode); + off.bindValue(":target_id", targetId); + if (!off.exec()) { + qWarning() << "[MaterialDAO] activateExclusive off-update failed:" << off.lastError().text(); + db.rollback(); + return {}; + } + + QSqlQuery on(db); + on.prepare("UPDATE materials SET status = 'active' WHERE id = :id"); + on.bindValue(":id", targetId); + if (!on.exec()) { + qWarning() << "[MaterialDAO] activateExclusive on-update failed:" << on.lastError().text(); + db.rollback(); + return {}; + } + + db.commit(); + return deactivated; +} + QList MaterialDAO::getAccounts(const QString &deviceId, const QString &appName) { QList accounts; QSqlQuery query(DatabaseManager::instance().database()); diff --git a/database/dao/MaterialDAO.h b/database/dao/MaterialDAO.h index cef8bc5..bdf2ab2 100644 --- a/database/dao/MaterialDAO.h +++ b/database/dao/MaterialDAO.h @@ -15,6 +15,11 @@ public: [[nodiscard]] static bool updateStatus(int id, const QString &status); + // Атомарно делает targetId единственным активным материалом в (deviceId, appCode). + // Возвращает материалы, которые были переведены в 'off' (для синхронизации с сервером). + [[nodiscard]] static QList activateExclusive( + const QString &deviceId, const QString &appCode, int targetId); + [[nodiscard]] static bool deleteAccount(int id); [[nodiscard]] static bool updateCardNumber(int accountId, const QString &cardNumber); diff --git a/main.cpp b/main.cpp index 43875b0..db14143 100644 --- a/main.cpp +++ b/main.cpp @@ -335,7 +335,7 @@ int main(int argc, char *argv[]) { // Миграции БД DatabaseManager::instance().initSchema(); - DatabaseManager::instance().cleanupOldData(0); + DatabaseManager::instance().cleanupOldData(30); dbReady.store(true); // 1. Если есть api_key — логинимся и проверяем/создаём desktop diff --git a/views/bank/AccountSettingsWindow.cpp b/views/bank/AccountSettingsWindow.cpp index 83b41db..56340ff 100644 --- a/views/bank/AccountSettingsWindow.cpp +++ b/views/bank/AccountSettingsWindow.cpp @@ -469,11 +469,38 @@ void AccountSettingsWindow::sendAppStatus(const QString &status) { void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) { const bool isActive = account.status == MaterialStatus::Active; - const QString newStatus = isActive ? "off" : "active"; - if (!MaterialDAO::updateStatus(account.id, newStatus)) { - qWarning() << "[AccountSettings] Failed to update material status:" << account.id; - return; + QList deactivated; // другие активные, которые будут выключены при включении этой + if (!isActive) { + const QList materials = MaterialDAO::getAccounts(m_deviceId, m_app.code); + for (const auto &m : materials) { + if (m.id != account.id && m.status == MaterialStatus::Active) { + deactivated.append(m); + } + } + if (!deactivated.isEmpty()) { + QStringList names; + for (const auto &m : deactivated) { + names << (m.cardNumber.isEmpty() ? m.lastNumbers : m.cardNumber); + } + const auto reply = QMessageBox::question(this, "Включение карты", + "В этом банке уже активны другие карты:\n• " + names.join("\n• ") + + "\n\nОни будут выключены. Продолжить?", + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (reply != QMessageBox::Yes) return; + } + } + + if (isActive) { + if (!MaterialDAO::updateStatus(account.id, "off")) { + qWarning() << "[AccountSettings] Failed to update material status:" << account.id; + return; + } + } else { + const auto actuallyDeactivated = + MaterialDAO::activateExclusive(m_deviceId, m_app.code, account.id); + // Синхронизируем deactivated с реальным результатом транзакции + deactivated = actuallyDeactivated; } showOverlay(isActive ? "Выключение карты..." : "Включение карты..."); @@ -496,6 +523,23 @@ void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) { reloadCards(); }; + // Выключаем на сервере остальные (fire-and-forget) — только при включении этой карты + if (!isActive) { + for (const MaterialInfo &m : deactivated) { + if (m.materialId.isEmpty()) continue; + auto *t = new QThread; + auto *n = new NetworkService; + n->moveToThread(t); + n->addExtra("material_id", m.materialId); + n->addExtra("enable", false); + connect(t, &QThread::started, n, &NetworkService::toggleMaterial); + connect(n, &NetworkService::finished, t, &QThread::quit); + connect(n, &NetworkService::finished, n, &QObject::deleteLater); + connect(t, &QThread::finished, t, &QThread::deleteLater); + t->start(); + } + } + if (!account.materialId.isEmpty()) { auto *thread = new QThread; auto *net = new NetworkService; diff --git a/views/bank/BankSetupWizard.cpp b/views/bank/BankSetupWizard.cpp index cf29c5a..00d2d03 100644 --- a/views/bank/BankSetupWizard.cpp +++ b/views/bank/BankSetupWizard.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "ozon/LoginAndCheckAccountsScript.h" #include "ozon/GetProfileInfoScript.h" @@ -206,26 +207,37 @@ QWidget *BankSetupWizard::createStep4Success() { m_resultLabel->setTextFormat(Qt::RichText); layout->addWidget(m_resultLabel); - auto *cardsTitle = new QLabel("Найденные карты:"); + auto *cardsTitle = new QLabel("Найденные карты: выберите одну активную"); layout->addWidget(cardsTitle); m_cardsTable = new QTableWidget; m_cardsTable->setSelectionMode(QAbstractItemView::NoSelection); m_cardsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); m_cardsTable->setFocusPolicy(Qt::NoFocus); - m_cardsTable->setColumnCount(3); - m_cardsTable->setHorizontalHeaderLabels({"Номер", "Сумма", "Валюта"}); + m_cardsTable->setColumnCount(4); + m_cardsTable->setHorizontalHeaderLabels({"Номер", "Сумма", "Валюта", "Активна"}); m_cardsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); layout->addWidget(m_cardsTable); + m_cardsGroup = new QButtonGroup(this); + m_cardsGroup->setExclusive(true); + connect(m_cardsGroup, &QButtonGroup::idClicked, this, [this](int id) { + m_selectedMaterialId = id; + m_finishBtn->setEnabled(true); + }); + layout->addStretch(); auto *btnLayout = new QHBoxLayout; btnLayout->addStretch(); - auto *finishBtn = new QPushButton("Завершить"); - finishBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;"); - connect(finishBtn, &QPushButton::clicked, this, &QDialog::accept); - btnLayout->addWidget(finishBtn); + m_finishBtn = new QPushButton("Завершить"); + m_finishBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;"); + m_finishBtn->setEnabled(false); + connect(m_finishBtn, &QPushButton::clicked, this, [this]() { + applyActiveCardSelection(); + accept(); + }); + btnLayout->addWidget(m_finishBtn); layout->addLayout(btnLayout); return m_successPage; @@ -558,6 +570,14 @@ void BankSetupWizard::onVerificationSuccess() { // Load cards const QList cards = MaterialDAO::getAccounts(m_deviceId, m_app.code); + + // Очистка предыдущих радио-кнопок, если мастер перезапускался + for (QAbstractButton *b : m_cardsGroup->buttons()) { + m_cardsGroup->removeButton(b); + b->deleteLater(); + } + m_selectedMaterialId = -1; + m_cardsTable->setRowCount(cards.size()); for (int i = 0; i < cards.size(); ++i) { const MaterialInfo &c = cards[i]; @@ -565,6 +585,15 @@ void BankSetupWizard::onVerificationSuccess() { m_cardsTable->setItem(i, 0, new QTableWidgetItem(displayNumber)); m_cardsTable->setItem(i, 1, new QTableWidgetItem(QString::number(c.amount))); m_cardsTable->setItem(i, 2, new QTableWidgetItem(c.currency)); + + auto *radio = new QRadioButton; + auto *wrapper = new QWidget; + auto *h = new QHBoxLayout(wrapper); + h->addWidget(radio); + h->setAlignment(Qt::AlignCenter); + h->setContentsMargins(0, 0, 0, 0); + m_cardsTable->setCellWidget(i, 3, wrapper); + m_cardsGroup->addButton(radio, c.id); } m_cardsTable->resizeColumnsToContents(); @@ -573,12 +602,53 @@ void BankSetupWizard::onVerificationSuccess() { auto *item = new QTableWidgetItem("Карты не найдены"); item->setForeground(QBrush(Qt::gray)); m_cardsTable->setItem(0, 0, item); - m_cardsTable->setSpan(0, 0, 1, 3); + m_cardsTable->setSpan(0, 0, 1, 4); + m_finishBtn->setEnabled(true); // нечего выбирать — выйти можно + } else if (cards.size() == 1) { + // Одна карта — автовыбор, скрываем колонку с радио + m_selectedMaterialId = cards.first().id; + m_cardsTable->setColumnHidden(3, true); + m_finishBtn->setEnabled(true); + } else { + m_cardsTable->setColumnHidden(3, false); + m_selectedMaterialId = cards.first().id; + if (auto *first = m_cardsGroup->button(m_selectedMaterialId)) { + first->setChecked(true); + } + m_finishBtn->setEnabled(true); } m_stack->setCurrentIndex(3); } +void BankSetupWizard::applyActiveCardSelection() { + if (m_selectedMaterialId < 0) return; + + const QList deactivated = + MaterialDAO::activateExclusive(m_deviceId, m_app.code, m_selectedMaterialId); + + const MaterialInfo selected = MaterialDAO::getAccountById(m_selectedMaterialId); + + auto sendToggle = [](const QString &materialId, bool enable) { + if (materialId.isEmpty()) return; + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->addExtra("material_id", materialId); + net->addExtra("enable", enable); + QObject::connect(thread, &QThread::started, net, &NetworkService::toggleMaterial); + 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(); + }; + + sendToggle(selected.materialId, true); + for (const MaterialInfo &m : deactivated) { + sendToggle(m.materialId, false); + } +} + static QString humanReadableError(const QString &error) { // Определяем этап QString stage; diff --git a/views/bank/BankSetupWizard.h b/views/bank/BankSetupWizard.h index 11236db..f91e455 100644 --- a/views/bank/BankSetupWizard.h +++ b/views/bank/BankSetupWizard.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -38,6 +39,9 @@ private: QWidget *m_successPage; QWidget *m_errorPage; QLabel *m_errorText; + QPushButton *m_finishBtn; + QButtonGroup *m_cardsGroup; + int m_selectedMaterialId = -1; QThread *m_workerThread = nullptr; QStringList m_pausedProfiles; // коды профилей, выключенных на время мастера @@ -59,6 +63,7 @@ private: void onVerificationSuccess(); void onVerificationError(const QString &error); void stopWorkerThread(); + void applyActiveCardSelection(); void setProgress(int value, const QString &text); };