1
0
forked from BRT/arc

Add PAN field detection and account number UI updates.

- Extend `ScreenXmlParser` with `findPanCardNumberInput` for detecting empty PAN fields in Ozon WebView.
- Update `PayByCardScript` to handle missing hints for recipient card input.
- Temporarily disable advanced account number parsing in `GetCardInfoScript`.
- Add `patchMaterial` method to `NetworkService` for updating account details.
- Update card management UI with refined "Switch" button logic and label changes.
This commit is contained in:
slava 2026-05-13 02:03:20 +07:00
parent 52d261a060
commit 3751406c36
8 changed files with 271 additions and 178 deletions

View File

@ -162,76 +162,77 @@ void GetCardInfoScript::doStart() {
}
// Достаём номер счёта: Настройки карты → Реквизиты карты → таб "Реквизиты счета".
// ВРЕМЕННО ОТКЛЮЧЕНО
QString accountNumber;
const Node settingsBtn = xmlScreenParser.findCardSettingsButton();
if (settingsBtn.x() > 0) {
AdbUtils::makeTap(m_deviceId, settingsBtn.x(), settingsBtn.y());
QThread::msleep(1500);
bool sheetFound = false;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isCardSettingsBottomSheet()) {
sheetFound = true;
break;
}
QThread::msleep(1000);
}
if (sheetFound) {
const Node requisitesItem = xmlScreenParser.findCardRequisitesMenuItem();
if (requisitesItem.x() > 0) {
AdbUtils::makeTap(m_deviceId, requisitesItem.x(), requisitesItem.y());
QThread::msleep(1500);
bool reqScreenFound = false;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isCardRequisitesScreen()) {
reqScreenFound = true;
break;
}
QThread::msleep(1000);
}
if (reqScreenFound) {
const Node accountTab = xmlScreenParser.findAccountRequisitesTab();
if (accountTab.x() > 0) {
AdbUtils::makeTap(m_deviceId, accountTab.x(), accountTab.y());
QThread::msleep(1500);
// Несколько попыток — данные могут подгружаться
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
accountNumber = xmlScreenParser.parseAccountNumber();
if (!accountNumber.isEmpty()) break;
QThread::msleep(1000);
}
qDebug() << "[Dushanbe::GetCardInfo] accountNumber for card" << i
<< ":" << accountNumber;
} else {
qWarning() << "[Dushanbe::GetCardInfo] Account tab not found for card" << i;
}
} else {
qWarning() << "[Dushanbe::GetCardInfo] Requisites screen not detected for card" << i;
}
} else {
qWarning() << "[Dushanbe::GetCardInfo] 'Реквизиты карты' menu item not found for card" << i;
}
} else {
qWarning() << "[Dushanbe::GetCardInfo] Card settings bottom sheet not detected for card" << i;
}
// Возврат на экран детали карты — bottom sheet и/или экран Реквизитов
for (int attempt = 0; attempt < 4; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isCardDetailScreen()) break;
AdbUtils::goBack(m_deviceId);
QThread::msleep(1200);
}
} else {
qWarning() << "[Dushanbe::GetCardInfo] 'Настройки карты' button not found for card" << i;
}
// const Node settingsBtn = xmlScreenParser.findCardSettingsButton();
// if (settingsBtn.x() > 0) {
// AdbUtils::makeTap(m_deviceId, settingsBtn.x(), settingsBtn.y());
// QThread::msleep(1500);
//
// bool sheetFound = false;
// for (int attempt = 0; attempt < 5; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// if (xmlScreenParser.isCardSettingsBottomSheet()) {
// sheetFound = true;
// break;
// }
// QThread::msleep(1000);
// }
//
// if (sheetFound) {
// const Node requisitesItem = xmlScreenParser.findCardRequisitesMenuItem();
// if (requisitesItem.x() > 0) {
// AdbUtils::makeTap(m_deviceId, requisitesItem.x(), requisitesItem.y());
// QThread::msleep(1500);
//
// bool reqScreenFound = false;
// for (int attempt = 0; attempt < 5; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// if (xmlScreenParser.isCardRequisitesScreen()) {
// reqScreenFound = true;
// break;
// }
// QThread::msleep(1000);
// }
//
// if (reqScreenFound) {
// const Node accountTab = xmlScreenParser.findAccountRequisitesTab();
// if (accountTab.x() > 0) {
// AdbUtils::makeTap(m_deviceId, accountTab.x(), accountTab.y());
// QThread::msleep(1500);
//
// // Несколько попыток — данные могут подгружаться
// for (int attempt = 0; attempt < 5; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// accountNumber = xmlScreenParser.parseAccountNumber();
// if (!accountNumber.isEmpty()) break;
// QThread::msleep(1000);
// }
// qDebug() << "[Dushanbe::GetCardInfo] accountNumber for card" << i
// << ":" << accountNumber;
// } else {
// qWarning() << "[Dushanbe::GetCardInfo] Account tab not found for card" << i;
// }
// } else {
// qWarning() << "[Dushanbe::GetCardInfo] Requisites screen not detected for card" << i;
// }
// } else {
// qWarning() << "[Dushanbe::GetCardInfo] 'Реквизиты карты' menu item not found for card" << i;
// }
// } else {
// qWarning() << "[Dushanbe::GetCardInfo] Card settings bottom sheet not detected for card" << i;
// }
//
// // Возврат на экран детали карты — bottom sheet и/или экран Реквизитов
// for (int attempt = 0; attempt < 4; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// if (xmlScreenParser.isCardDetailScreen()) break;
// AdbUtils::goBack(m_deviceId);
// QThread::msleep(1200);
// }
// } else {
// qWarning() << "[Dushanbe::GetCardInfo] 'Настройки карты' button not found for card" << i;
// }
MaterialInfo account;
account.deviceId = device.id;

View File

@ -415,103 +415,104 @@ void GetCardInfoScript::doStart() {
}
// 2.5. Достаём номер счёта (общий для всех карт): Основной счёт → Посмотреть реквизиты
QString accountNumber;
{
bool needFetch = false;
for (const MaterialInfo &c : cards) {
const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers);
if (dbCard.id != -1 && dbCard.accountNumber.isEmpty()) { needFetch = true; break; }
}
if (needFetch) {
// Возвращаемся на главный экран — критерий именно по наличию кнопки
// "Основной счёт ... ₽". isHomeScreen() для Озона нестрогий
// (Перевести/Пополнить присутствуют и на других экранах).
Node mainAcc;
for (int attempt = 0; attempt < 6; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
mainAcc = xmlScreenParser.findMainAccountButton();
if (mainAcc.x() > 0) break;
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
}
if (mainAcc.x() > 0) {
AdbUtils::makeTap(m_deviceId, mainAcc.x(), mainAcc.y());
QThread::msleep(2000);
// Скроллим WebView, пока "Посмотреть реквизиты" не окажется в верхней половине,
// чтобы тап не ушёл в системный нав-бар.
Node showReq;
const int targetMaxY = device.screenHeight / 2;
for (int attempt = 0; attempt < 6; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
Node candidate = xmlScreenParser.findShowRequisitesButton();
if (candidate.x() > 0 && candidate.y() <= targetMaxY) {
showReq = candidate;
break;
}
AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2,
device.screenHeight * 2 / 3,
device.screenWidth / 2,
device.screenHeight / 3);
QThread::msleep(800);
}
if (showReq.x() > 0) {
AdbUtils::makeTap(m_deviceId, showReq.x(), showReq.y());
QThread::msleep(2000);
bool reqScreen = false;
for (int attempt = 0; attempt < 6; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isAccountRequisitesScreen()) {
reqScreen = true;
break;
}
QThread::msleep(1000);
}
if (reqScreen) {
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
accountNumber = xmlScreenParser.parseAccountNumberFromRequisites();
if (!accountNumber.isEmpty()) break;
QThread::msleep(800);
}
qDebug() << "[GetCardInfo] accountNumber:" << accountNumber;
} else {
qWarning() << "[GetCardInfo] Account requisites screen not detected";
}
} else {
qWarning() << "[GetCardInfo] 'Посмотреть реквизиты' not reached";
}
} else {
qWarning() << "[GetCardInfo] 'Основной счёт' button not found on home";
}
// Возврат на главный — критерий по кнопке "Основной счёт ... ₽"
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.findMainAccountButton().x() > 0) break;
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
}
// Записываем номер счёта во все материалы device/app
if (!accountNumber.isEmpty()) {
for (const MaterialInfo &c : cards) {
MaterialInfo upd = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers);
if (upd.id == -1) continue;
upd.accountNumber = accountNumber;
upd.updateTime = QDateTime::currentDateTimeUtc();
if (!MaterialDAO::upsertAccount(upd)) {
qWarning() << "[GetCardInfo] Failed to save accountNumber for" << upd.lastNumbers;
}
}
}
}
}
// ВРЕМЕННО ОТКЛЮЧЕНО
// QString accountNumber;
// {
// bool needFetch = false;
// for (const MaterialInfo &c : cards) {
// const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers);
// if (dbCard.id != -1 && dbCard.accountNumber.isEmpty()) { needFetch = true; break; }
// }
//
// if (needFetch) {
// // Возвращаемся на главный экран — критерий именно по наличию кнопки
// // "Основной счёт ... ₽". isHomeScreen() для Озона нестрогий
// // (Перевести/Пополнить присутствуют и на других экранах).
// Node mainAcc;
// for (int attempt = 0; attempt < 6; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// mainAcc = xmlScreenParser.findMainAccountButton();
// if (mainAcc.x() > 0) break;
// AdbUtils::goBack(m_deviceId);
// QThread::msleep(1500);
// }
//
// if (mainAcc.x() > 0) {
// AdbUtils::makeTap(m_deviceId, mainAcc.x(), mainAcc.y());
// QThread::msleep(2000);
//
// // Скроллим WebView, пока "Посмотреть реквизиты" не окажется в верхней половине,
// // чтобы тап не ушёл в системный нав-бар.
// Node showReq;
// const int targetMaxY = device.screenHeight / 2;
// for (int attempt = 0; attempt < 6; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Node candidate = xmlScreenParser.findShowRequisitesButton();
// if (candidate.x() > 0 && candidate.y() <= targetMaxY) {
// showReq = candidate;
// break;
// }
// AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2,
// device.screenHeight * 2 / 3,
// device.screenWidth / 2,
// device.screenHeight / 3);
// QThread::msleep(800);
// }
//
// if (showReq.x() > 0) {
// AdbUtils::makeTap(m_deviceId, showReq.x(), showReq.y());
// QThread::msleep(2000);
//
// bool reqScreen = false;
// for (int attempt = 0; attempt < 6; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// if (xmlScreenParser.isAccountRequisitesScreen()) {
// reqScreen = true;
// break;
// }
// QThread::msleep(1000);
// }
//
// if (reqScreen) {
// for (int attempt = 0; attempt < 5; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// accountNumber = xmlScreenParser.parseAccountNumberFromRequisites();
// if (!accountNumber.isEmpty()) break;
// QThread::msleep(800);
// }
// qDebug() << "[GetCardInfo] accountNumber:" << accountNumber;
// } else {
// qWarning() << "[GetCardInfo] Account requisites screen not detected";
// }
// } else {
// qWarning() << "[GetCardInfo] 'Посмотреть реквизиты' not reached";
// }
// } else {
// qWarning() << "[GetCardInfo] 'Основной счёт' button not found on home";
// }
//
// // Возврат на главный — критерий по кнопке "Основной счёт ... ₽"
// for (int attempt = 0; attempt < 5; ++attempt) {
// xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// if (xmlScreenParser.findMainAccountButton().x() > 0) break;
// AdbUtils::goBack(m_deviceId);
// QThread::msleep(1500);
// }
//
// // Записываем номер счёта во все материалы device/app
// if (!accountNumber.isEmpty()) {
// for (const MaterialInfo &c : cards) {
// MaterialInfo upd = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers);
// if (upd.id == -1) continue;
// upd.accountNumber = accountNumber;
// upd.updateTime = QDateTime::currentDateTimeUtc();
// if (!MaterialDAO::upsertAccount(upd)) {
// qWarning() << "[GetCardInfo] Failed to save accountNumber for" << upd.lastNumbers;
// }
// }
// }
// }
// }
// 3. Отправляем материалы (карты) — после OCR, когда номера карт уже в БД
const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode);
@ -530,7 +531,7 @@ void GetCardInfoScript::doStart() {
QJsonObject materialBody;
materialBody["bank_profile_id"] = updatedApp.bankProfileId;
materialBody["card_number"] = dbCard.cardNumber;
// materialBody["account_number"] = dbCard.accountNumber;
materialBody["account_number"] = dbCard.accountNumber;
materialBody["name"] = updatedApp.fullName;
materialBody["state"] = materialStatusToString(dbCard.status) == "active" ? "enabled" : "disabled";
materialBody["currency"] = currencyCodes.value(dbCard.currency.toUpper(), 643);

View File

@ -248,6 +248,12 @@ void PayByCardScript::makePayment(
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
cardInput = xmlScreenParser.findEditTextByHint("Номер карты получателя");
if (cardInput.isEmpty()) {
// На части сборок Ozon WebView hint у поля PAN пустой, а лейбла
// "Номер карты получателя" на экране нет (есть только "КУДА").
// Поле PAN всегда внутри элемента resource-id="movable-pan-iframe".
cardInput = xmlScreenParser.findPanCardNumberInput();
}
if (!cardInput.isEmpty()) break;
qDebug() << "[PayByCard] Waiting for 'Номер карты получателя' input, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);

View File

@ -176,6 +176,47 @@ Node ScreenXmlParser::findEditTextByHint(const QString &hintPrefix) {
return best;
}
static QDomElement findPanIframeElement(const QDomElement &root) {
if (root.isNull()) return {};
if (root.attribute("resource-id") == "movable-pan-iframe") return root;
for (QDomElement child = root.firstChildElement(); !child.isNull(); child = child.nextSiblingElement()) {
QDomElement found = findPanIframeElement(child);
if (!found.isNull()) return found;
}
return {};
}
static QDomElement findFirstEditTextDescendant(const QDomElement &root) {
if (root.isNull()) return {};
if (root.attribute("class") == "android.widget.EditText") return root;
for (QDomElement child = root.firstChildElement(); !child.isNull(); child = child.nextSiblingElement()) {
QDomElement found = findFirstEditTextDescendant(child);
if (!found.isNull()) return found;
}
return {};
}
Node ScreenXmlParser::findPanCardNumberInput() {
const QDomElement iframe = findPanIframeElement(m_xml);
if (iframe.isNull()) return {};
const QDomElement edit = findFirstEditTextDescendant(iframe);
if (edit.isNull()) return {};
Node node;
if (QRegularExpressionMatch m = boundsRegex.match(edit.attribute("bounds")); m.hasMatch()) {
node.setBounds(m.captured(1).toInt(), m.captured(2).toInt(),
m.captured(3).toInt(), m.captured(4).toInt());
}
node.className = edit.attribute("class");
node.text = edit.attribute("text");
node.contentDesc = edit.attribute("content-desc");
node.resourceId = edit.attribute("resource-id");
node.hint = edit.attribute("hint");
node.clickable = edit.attribute("clickable") == "true";
node.focusable = edit.attribute("focusable") == "true";
return node;
}
Node ScreenXmlParser::findButtonNode(const QString &text, const bool contains) {
for (const Node &node: m_nodes) {
if (contains) {

View File

@ -30,6 +30,12 @@ public:
Node findEditTextByHint(const QString &hintPrefix);
// Возвращает EditText внутри Ozon "movable-pan-iframe" — поле номера карты
// получателя на экране "Перевод по номеру карты". Нужно как fallback, когда
// WebView не отдаёт hint и на экране нет TextView-лейбла "Номер карты
// получателя" (лейбл — просто "КУДА").
Node findPanCardNumberInput();
Node findButtonNode(const QString &text, bool contains);
QList<Node> findAllButtons();

View File

@ -1010,6 +1010,41 @@ void NetworkService::patchBankProfile() {
emit finished();
}
void NetworkService::patchMaterial() {
if (!ensureValidToken()) { emit finishedWithResult(-1); emit finished(); return; }
const QString materialId = m_extraData.value("material_id").toString();
if (materialId.isEmpty()) {
qWarning() << "[NetworkService] patchMaterial: material_id is empty";
emit finishedWithResult(-1); emit finished();
return;
}
QJsonObject body;
if (m_extraData.contains("account_number")) {
body["account_number"] = m_extraData.value("account_number").toString();
}
if (m_extraData.contains("state")) {
body["state"] = m_extraData.value("state").toString();
}
const QString path = m_apiBase + m_pathMaterial + "/" + materialId;
const QByteArray bodyJson = QJsonDocument(body).toJson(QJsonDocument::Indented);
qDebug().noquote() << "[NetworkService] patchMaterial PATCH" << path
<< "\nbody:\n" << bodyJson;
const QJsonValue result = patchJson(path, body, true);
if (result.isNull() || result.isUndefined()) {
qWarning() << "[NetworkService] patchMaterial failed:" << materialId;
emit finishedWithResult(-1);
} else {
qDebug() << "[NetworkService] patchMaterial done:" << materialId;
emit finishedWithResult(0);
}
emit finished();
}
void NetworkService::toggleMaterial() {
if (!ensureValidToken()) { emit finishedWithResult(-1); emit finished(); return; }

View File

@ -32,6 +32,8 @@ public slots:
void postMaterial();
void patchMaterial();
void toggleMaterial();
void postDeviceScreenshot();

View File

@ -97,6 +97,7 @@ AccountSettingsWindow::AccountSettingsWindow(
m_cardsBtn = new QPushButton("Проверить аккаунты");
m_cardsBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 12px;");
connect(m_cardsBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startGetCardInfo);
m_cardsBtn->hide();
actionsRow->addWidget(m_cardsBtn);
actionsRow->addStretch();
@ -572,7 +573,7 @@ void AccountSettingsWindow::reloadCards() {
m_cardsTable->setRowCount(accounts.size());
m_cardsTable->setColumnCount(7);
m_cardsTable->setHorizontalHeaderLabels({
"Сервер", "Статус", "Время обновления", "Номер", "Сумма", "Валюта", ""
"Сервер", "Статус", "Время обновления", "Номер карты", "Сумма", "Валюта", ""
});
m_cardsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
@ -597,7 +598,7 @@ 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);
@ -613,17 +614,17 @@ void AccountSettingsWindow::reloadCards() {
currItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_cardsTable->setItem(k, 5, currItem);
// Кнопка вкл/выкл
auto *toggleBtn = new QPushButton(isActive ? "Выключить" : "Включить", m_cardsTable);
toggleBtn->setStyleSheet(isActive
? "background-color: red; color: white;"
: "background-color: green; color: white;");
// Кнопка переключения: только для неактивных — включит этот и выключит остальные
if (!isActive) {
auto *toggleBtn = new QPushButton("Переключить", m_cardsTable);
toggleBtn->setStyleSheet("background-color: green; color: white;");
m_cardsTable->setCellWidget(k, 6, toggleBtn);
connect(toggleBtn, &QPushButton::clicked, this, [this, acc]() {
toggleMaterialStatus(acc);
});
}
}
m_cardsTable->resizeColumnsToContents();
m_cardsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);