1
0
forked from BRT/arc

Add exclusive material activation logic to MaterialDAO and improve card selection in BankSetupWizard.

This commit is contained in:
slava 2026-04-23 20:44:44 +07:00
parent efc2da5cd0
commit 0a460d10a0
7 changed files with 222 additions and 25 deletions

View File

@ -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));

View File

@ -142,6 +142,58 @@ MaterialInfo parseAccount(const QSqlQuery &query) {
return acc;
}
QList<MaterialInfo> MaterialDAO::activateExclusive(
const QString &deviceId, const QString &appCode, const int targetId) {
QList<MaterialInfo> 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<MaterialInfo> MaterialDAO::getAccounts(const QString &deviceId, const QString &appName) {
QList<MaterialInfo> accounts;
QSqlQuery query(DatabaseManager::instance().database());

View File

@ -15,6 +15,11 @@ public:
[[nodiscard]] static bool updateStatus(int id, const QString &status);
// Атомарно делает targetId единственным активным материалом в (deviceId, appCode).
// Возвращает материалы, которые были переведены в 'off' (для синхронизации с сервером).
[[nodiscard]] static QList<MaterialInfo> activateExclusive(
const QString &deviceId, const QString &appCode, int targetId);
[[nodiscard]] static bool deleteAccount(int id);
[[nodiscard]] static bool updateCardNumber(int accountId, const QString &cardNumber);

View File

@ -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

View File

@ -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<MaterialInfo> deactivated; // другие активные, которые будут выключены при включении этой
if (!isActive) {
const QList<MaterialInfo> 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;

View File

@ -4,6 +4,7 @@
#include <QHBoxLayout>
#include <QHeaderView>
#include <QCoreApplication>
#include <QRadioButton>
#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("<b>Найденные карты:</b>");
auto *cardsTitle = new QLabel("<b>Найденные карты:</b> выберите одну активную");
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<MaterialInfo> 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<MaterialInfo> 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;

View File

@ -1,5 +1,6 @@
#pragma once
#include <QButtonGroup>
#include <QDialog>
#include <QLabel>
#include <QLineEdit>
@ -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);
};