Add exclusive material activation logic to MaterialDAO and improve card selection in BankSetupWizard.
This commit is contained in:
parent
efc2da5cd0
commit
0a460d10a0
@ -251,16 +251,20 @@ 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 (candidate.isEmpty() || requisites.isEmpty()) {
|
||||
// Если всё ещё на домашнем экране — тап по карточке не сработал, повторяем
|
||||
if (xmlScreenParser.isHomeScreen()) {
|
||||
cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers);
|
||||
if (!cardBtn.isEmpty()) {
|
||||
@ -268,12 +272,29 @@ void GetCardInfoScript::doStart() {
|
||||
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));
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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);
|
||||
|
||||
2
main.cpp
2
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
|
||||
|
||||
@ -469,12 +469,39 @@ 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)) {
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user