#include "GetCardInfoScript.h" #include #include #include #include #include #include "adb/AdbUtils.h" #include "dao/MaterialDAO.h" #include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/SettingsDAO.h" #include "dump/TaskDumpRecorder.h" #include "net/NetworkService.h" #include "AppLogger.h" namespace Ozon { GetCardInfoScript::GetCardInfoScript( QString deviceId, QString appCode, QString lastNumbers, QObject *parent ) : CommonScript(parent), m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)), m_lastNumbers(std::move(lastNumbers)) { } GetCardInfoScript::~GetCardInfoScript() = default; void GetCardInfoScript::doStart() { DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); if (device.id.isEmpty() || app.id == -1) { m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; qCritical() << m_error; emit finishedWithResult(m_error); return; } const TaskDumpMeta dumpMeta{}; TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); // Читаем текущий экран xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); // Закрываем рекламный bottom sheet, если открыт if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { qDebug() << "[GetCardInfo] Ad banner detected, closing..."; AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y()); QThread::msleep(1000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); } // Свежий дамп перед проверкой домашнего экрана xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); // Если не на домашнем экране — перезапускаем приложение if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[GetCardInfo] Not on home screen, restarting app..."; if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ")"; qWarning() << m_error; AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; } xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); } // Закрываем баннеры на главной tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight); QThread::msleep(500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); // Переходим в профиль и парсим имя и телефон (только если ещё нет в БД) const bool hasProfileInfo = !app.fullName.isEmpty() && !app.phone.isEmpty(); Node userNameNode; if (!hasProfileInfo) { userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight); } if (!hasProfileInfo && !userNameNode.isEmpty()) { qDebug() << "[GetCardInfo] Navigating to profile to parse name/phone..."; AdbUtils::makeTap(m_deviceId, userNameNode.x(), userNameNode.y()); QThread::msleep(2500); for (int attempt = 0; attempt < 5; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); if (xmlScreenParser.isProfileScreen()) break; QThread::msleep(1000); } if (xmlScreenParser.isProfileScreen()) { const QString fullName = xmlScreenParser.parseProfileFullName(); const QString phone = xmlScreenParser.parseProfilePhone(); qDebug() << "[GetCardInfo] Profile parsed - fullName:" << fullName << "phone:" << phone; BankProfileDAO::updateProfileInfo(app.id, fullName, phone); } else { qWarning() << "[GetCardInfo] Profile screen not reached, skipping profile parse"; } AdbUtils::goBack(m_deviceId); QThread::msleep(1500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); } else { qDebug() << "[GetCardInfo] Profile info already in DB or username not found, skipping profile parse"; } // Проверяем что на домашнем экране перед поиском карт xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); if (!xmlScreenParser.isHomeScreen()) { qDebug() << "[GetCardInfo] Not on home screen before card search, restarting..."; if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen before card search: " + device.name + " (" + m_deviceId + ")"; qWarning() << m_error; AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; } tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight); QThread::msleep(500); } // Ждём пока все карты подгрузятся — до 5 попыток QList cards; for (int attempt = 0; attempt < 5; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); cards = xmlScreenParser.parseCardsOnHomeScreen(); if (!cards.isEmpty()) break; qDebug() << "[GetCardInfo] No cards found on home screen, attempt" << attempt + 1 << "/ 5"; QThread::msleep(1500); } if (cards.isEmpty()) { m_error = "No cards found on home screen: " + m_deviceId; qWarning() << m_error; AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; } qDebug() << "[GetCardInfo] Found" << cards.size() << "card(s) on home screen"; // Апсёртим аккаунты для всех найденных карт for (MaterialInfo &card : cards) { card.deviceId = device.id; card.appCode = m_appCode; card.updateTime = QDateTime::currentDateTimeUtc(); card.status = MaterialStatus::Active; if (!MaterialDAO::upsertAccount(card)) { qWarning() << "[GetCardInfo] Failed to upsert account for card" << card.lastNumbers; } else { qDebug() << "[GetCardInfo] Upserted account:" << card.lastNumbers << card.description << card.amount; } } // Сохраняем баланс домашнего экрана в application const double totalAmount = cards.isEmpty() ? 0.0 : cards[0].amount; if (totalAmount > 0.0) { BankProfileDAO::updateAmount(app.id, totalAmount); } // Синхронная отправка на сервер (NetworkService блокируется через QEventLoop внутри) NetworkService ns; ns.setDeviceId(m_deviceId); // 1. Создаём девайс если нет api_id if (device.apiId.isEmpty()) { QJsonObject deviceBody; deviceBody["desktop_id"] = SettingsDAO::get("desktop_id"); deviceBody["status"] = "ONLINE"; deviceBody["name"] = device.name; deviceBody["screen_width"] = device.screenWidth; deviceBody["screen_height"]= device.screenHeight; deviceBody["battery"] = device.battery; ns.setPayload(QJsonDocument(deviceBody).toJson()); ns.createDevice(); qDebug() << "[GetCardInfo] createDevice done"; } // 2. Отправляем банковский профиль (используем свежие данные из БД — имя/телефон уже спарсены) { const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode); const QStringList parts = freshApp.fullName.split(' ', Qt::SkipEmptyParts); // ISO 4217 numeric currency codes const QMap currencyCodes = { {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} }; const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643); QJsonObject profileBody; profileBody["first_name"] = parts.value(0); profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : ""; profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); profileBody["bank_name"] = m_appCode; // Телефон: убираем пробелы, оставляем "+" const QString phone = freshApp.phone.simplified().remove(' '); profileBody["phone_number"] = phone; profileBody["balance"] = QString::number(qRound64(totalAmount * 100)); profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled"; profileBody["device_id"] = device.apiId; profileBody["currency"] = currencyCode; ns.setPayload(QJsonDocument(profileBody).toJson()); if (freshApp.bankProfileId.isEmpty()) { ns.addExtra("app_id", freshApp.id); ns.postBankProfile(); // Проверяем что bankProfileId сохранился const BankProfileInfo afterPost = BankProfileDAO::getApplication(device.id, m_appCode); if (afterPost.bankProfileId.isEmpty()) { const QString err = "postBankProfile failed for " + m_deviceId; qWarning() << "[GetCardInfo]" << err; AppLogger::log("ozon/GetCardInfo", m_deviceId, err, AdbUtils::captureScreenshotBytes(m_deviceId), "FETCH_PROFILE"); } else { qDebug() << "[GetCardInfo] postBankProfile done"; } } else { ns.addExtra("bank_profile_id", freshApp.bankProfileId); ns.patchBankProfile(); qDebug() << "[GetCardInfo] patchBankProfile done"; } } // Для каждой карты переходим на её экран и получаем полный номер через OCR for (int i = 0; i < cards.size(); ++i) { const MaterialInfo &card = cards[i]; // Если передан конкретный lastNumbers — обрабатываем только его if (!m_lastNumbers.isEmpty() && card.lastNumbers != m_lastNumbers) continue; qDebug() << "[GetCardInfo] Getting card number for" << card.lastNumbers; // Если номер карты уже есть в БД — сумма уже обновлена через upsert, навигация не нужна MaterialInfo existing = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); if (existing.id != -1 && !existing.cardNumber.isEmpty()) { qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "already has number, skipping"; continue; } // Тапаем по кнопке конкретной карты — каждая открывает свой экран Node cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers); if (cardBtn.isEmpty()) { qWarning() << "[GetCardInfo] Card button not found for" << card.lastNumbers; continue; } AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); // Скроллим WebView вверх, пока "Показать" не окажется в верхней трети экрана. // На устройствах с короткой высотой (например, SM-A165F, h=2340) кнопка "Показать" // находится за bottom navigation, и тап уходит в нав-бар. const int targetTopY = device.screenHeight / 3; for (int scrollAttempt = 0; scrollAttempt < 6; ++scrollAttempt) { QThread::msleep(scrollAttempt == 0 ? 2000 : 600); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); Node candidate = xmlScreenParser.findButtonNode("Показать", false); if (candidate.isEmpty()) { if (xmlScreenParser.isHomeScreen()) { cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers); if (!cardBtn.isEmpty()) { qDebug() << "[GetCardInfo] Still on home before scroll, re-tapping card" << card.lastNumbers; AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); } } continue; } if (candidate.y() <= targetTopY) { qDebug() << "[GetCardInfo] 'Показать' already at top y=" << candidate.y(); break; } const int swipeFromY = candidate.y(); const int swipeToY = device.screenHeight / 6; qDebug() << "[GetCardInfo] Scrolling 'Показать' up:" << swipeFromY << "->" << swipeToY; AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, swipeFromY, device.screenWidth / 2, swipeToY); } // Ждём экран карты и стабильной геометрии блока реквизитов. // Без этого тап по "Показать" во время reflow уезжает ниже — в "Защиту от мошенников". // Стабильность: "Показать" совпадает по y1 с "Реквизиты карты" И координаты не меняются в двух подряд дампах. Node showBtn; int prevShowY = -1; for (int attempt = 0; attempt < 10; ++attempt) { QThread::msleep(attempt == 0 ? 2000 : 500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); 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()) { 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; } // "Показать" должен быть в одной строке с "Реквизиты карты" 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 stable for card" << card.lastNumbers; AdbUtils::goBack(m_deviceId); QThread::msleep(1500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); continue; } qDebug() << "[GetCardInfo] 'Показать' at" << showBtn.x() << showBtn.y(); AdbUtils::makeTap(m_deviceId, showBtn.x(), showBtn.y()); // Ждём появления полного номера карты в XML дампе QString cardNumber; for (int attempt = 0; attempt < 10; ++attempt) { QThread::msleep(2000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); cardNumber = xmlScreenParser.findFullCardNumber(); if (!cardNumber.isEmpty()) { qDebug() << "[GetCardInfo] Card number found on attempt" << attempt + 1; break; } // Закрываем баннер "Понятно" и повторно нажимаем "Показать" Node bannerBtn = xmlScreenParser.findTextNode("Понятно"); if (!bannerBtn.isEmpty()) { qDebug() << "[GetCardInfo] Banner 'Понятно' detected, closing..."; AdbUtils::makeTap(m_deviceId, bannerBtn.x(), bannerBtn.y()); QThread::msleep(1500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); Node retryShowBtn = xmlScreenParser.findButtonNode("Показать", false); if (!retryShowBtn.isEmpty()) { qDebug() << "[GetCardInfo] Re-tapping 'Показать' after banner close"; AdbUtils::makeTap(m_deviceId, retryShowBtn.x(), retryShowBtn.y()); } continue; } // "Подождите" — загрузка в процессе, просто ждём if (!xmlScreenParser.findButtonNode("Подождите", false).isEmpty()) { qDebug() << "[GetCardInfo] 'Подождите' visible, waiting... attempt" << attempt + 1 << "/ 10"; continue; } // "Скрыть" видна — номер должен быть раскрыт, но парсер не нашёл if (!xmlScreenParser.findButtonNode("Скрыть", false).isEmpty()) { qDebug() << "[GetCardInfo] 'Скрыть' visible but card number not parsed, attempt" << attempt + 1 << "/ 10"; continue; } // Повторно нажимаем "Показать" если тап не сработал Node retryShowBtn = xmlScreenParser.findButtonNode("Показать", false); if (!retryShowBtn.isEmpty()) { qDebug() << "[GetCardInfo] Re-tapping 'Показать', attempt" << attempt + 1 << "/ 10"; AdbUtils::makeTap(m_deviceId, retryShowBtn.x(), retryShowBtn.y()); } else { qDebug() << "[GetCardInfo] Card number not visible, attempt" << attempt + 1 << "/ 10"; } } if (!cardNumber.isEmpty()) { qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "full number:" << cardNumber; // Получаем id аккаунта из БД и сохраняем номер карты MaterialInfo saved = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); if (saved.id != -1) { MaterialDAO::updateCardNumber(saved.id, cardNumber); } } else { const QString err = "Card number not found for " + card.lastNumbers; qWarning() << "[GetCardInfo]" << err; MaterialInfo saved = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); if (saved.id != -1) { MaterialDAO::updateAccountById(saved.id, materialStatusToString(MaterialStatus::Off), saved.description, saved.currency); } AppLogger::log("ozon/GetCardInfo", m_deviceId, err, AdbUtils::captureScreenshotBytes(m_deviceId), "FETCH_PROFILE"); } // Возвращаемся на домашний экран для следующей карты if (i < cards.size() - 1) { AdbUtils::goBack(m_deviceId); QThread::msleep(2000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); } } // 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; } } } } } // 3. Отправляем материалы (карты) — после OCR, когда номера карт уже в БД const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode); if (!updatedApp.bankProfileId.isEmpty()) { for (const MaterialInfo &card : cards) { const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); if (dbCard.cardNumber.isEmpty()) continue; if (!dbCard.materialId.isEmpty()) { qDebug() << "[GetCardInfo] material already exists for card" << dbCard.lastNumbers << ", skipping"; continue; } const QMap currencyCodes = { {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} }; QJsonObject materialBody; materialBody["bank_profile_id"] = updatedApp.bankProfileId; materialBody["card_number"] = dbCard.cardNumber; // 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); ns.setPayload(QJsonDocument(materialBody).toJson()); ns.addExtra("account_id", dbCard.id); ns.postMaterial(); qDebug() << "[GetCardInfo] postMaterial done for card" << dbCard.lastNumbers; } } else { qWarning() << "[GetCardInfo] bankProfileId empty, skipping materials"; } AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); } } // namespace Ozon