#include "GetLastTransactionsScript.h" #include #include #include #include #include #include #include "adb/AdbUtils.h" #include "dao/AccountDAO.h" #include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" #include "dao/TransactionDAO.h" #include "time/DateUtils.h" #include "AppLogger.h" static QString recognizeTextFromImage(const QByteArray &pngData) { auto *api = new tesseract::TessBaseAPI(); if (api->Init(nullptr, "rus+eng")) { qWarning() << "[OCR] Failed to init Tesseract"; delete api; return {}; } Pix *pix = pixReadMem(reinterpret_cast(pngData.constData()), static_cast(pngData.size())); if (!pix) { qWarning() << "[OCR] Failed to read image"; api->End(); delete api; return {}; } // Convert to grayscale for better OCR accuracy Pix *gray = pixConvertRGBToGray(pix, 0.0f, 0.0f, 0.0f); pixDestroy(&pix); if (!gray) { qWarning() << "[OCR] Failed to convert to grayscale"; api->End(); delete api; return {}; } api->SetImage(gray); // adb screencap PNGs lack DPI metadata; set 300 DPI so Tesseract // interprets character sizes correctly api->SetSourceResolution(300); char *text = api->GetUTF8Text(); QString result = QString::fromUtf8(text).trimmed(); delete[] text; pixDestroy(&gray); api->End(); delete api; return result; } static QString extractTransactionId(const QString &ocrText) { // Ищем по слову "операции" — OCR часто искажает "ID" перед ним // \S{1,3} вместо точного "ID" покрывает любые OCR-ошибки (1О, 1Ю, ID, 10 и т.д.) QRegularExpression reId(R"(\S{1,3}\s+операции\s*=?\s*([A-Za-zА-Яа-яЁё0-9]+))"); if (auto m = reId.match(ocrText); m.hasMatch()) return m.captured(1); // "№ Ф-2026-25235663" QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))"); if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1); return {}; } namespace Ozon { GetLastTransactionsScript::GetLastTransactionsScript( QString deviceId, QString appCode, int maxCount, QObject *parent ) : CommonScript(parent), m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)), m_maxCount(maxCount) { } GetLastTransactionsScript::~GetLastTransactionsScript() = default; void GetLastTransactionsScript::doStart() { const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, 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; } xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); // Закрываем рекламный bottom sheet, если открыт if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { qDebug() << "[GetLastTransactions] 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() << "[GetLastTransactions] Not on home screen, restarting app..."; if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + m_deviceId; qWarning() << m_error; AppLogger::log("ozon/GetLastTransactions", m_deviceId, m_error, AdbUtils::captureScreenshotBytes(m_deviceId), "FETCH_TRANSACTIONS"); 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)); // Скроллим вниз до кнопки "Все" и тапаем по ней // Кнопка "Все" на домашнем экране находится у границы навбара (y ≈ 2190), // поэтому сначала скроллим вниз, чтобы она поднялась выше навигационной панели const int navBarTop = device.screenHeight - (device.screenHeight / 10); // ~90% высоты экрана Node allBtn; for (int i = 0; i < 10; ++i) { // Сначала скроллим, потом ищем const int x = device.screenWidth / 2; const int offset = device.screenHeight / 4; AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - offset, x, offset); QThread::msleep(1000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); allBtn = xmlScreenParser.findButtonNode("Все", false); if (!allBtn.isEmpty() && allBtn.y() > 0 && allBtn.y() < navBarTop) { break; } allBtn = {}; qDebug() << "[GetLastTransactions] 'Все' not found above navbar, attempt" << i + 1 << "/ 10"; } if (allBtn.isEmpty()) { m_error = "Button 'Все' not found after scrolling: " + m_deviceId; qWarning() << m_error; emit finishedWithResult(m_error); return; } qDebug() << "[GetLastTransactions] Found 'Все' at" << allBtn.x() << allBtn.y(); AdbUtils::makeTap(m_deviceId, allBtn.x(), allBtn.y()); QThread::msleep(2000); // Ждём загрузки экрана "Операции" bool loaded = false; for (int attempt = 0; attempt < 10; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); if (xmlScreenParser.isTransactionListScreen()) { loaded = true; break; } qDebug() << "[GetLastTransactions] Waiting for transaction list screen, attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); } if (!loaded) { m_error = "Transaction list screen not loaded: " + m_deviceId; qWarning() << m_error; emit finishedWithResult(m_error); return; } // Ждём появления транзакций (могут подгружаться) QList txButtons; for (int attempt = 0; attempt < 5; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); txButtons = xmlScreenParser.findTransactionButtons(8); if (!txButtons.isEmpty()) break; qDebug() << "[GetLastTransactions] No transactions yet, attempt" << attempt + 1 << "/ 5"; QThread::msleep(1500); } qDebug() << "[GetLastTransactions] Found" << txButtons.size() << "transaction buttons"; // Находим accountId для сохранения транзакций int accountId = -1; const QList accounts = AccountDAO::getAccounts(m_deviceId, m_appCode); if (!accounts.isEmpty()) { accountId = accounts.first().id; qDebug() << "[GetLastTransactions] Using accountId:" << accountId; } else { qWarning() << "[GetLastTransactions] No account found for" << m_deviceId << m_appCode; } // Для каждой транзакции: клик → берём дату из деталей → назад QList transactions; for (int i = 0; i < txButtons.size(); ++i) { const Node &btn = txButtons[i]; // Если кнопка внизу экрана (за навбаром) — скроллим до неё const int navBarTop = device.screenHeight - (device.screenHeight / 10); if (btn.y() <= 0 || btn.y() >= navBarTop) { qDebug() << "[GetLastTransactions] Transaction" << i + 1 << "is off-screen (y=" << btn.y() << "), scrolling..."; for (int s = 0; s < 5; ++s) { const int x = device.screenWidth / 2; const int offset = device.screenHeight / 4; AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - offset, x, offset); QThread::msleep(1000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); txButtons = xmlScreenParser.findTransactionButtons(8); if (i < txButtons.size() && txButtons[i].y() > 0 && txButtons[i].y() < navBarTop) { break; } } if (i >= txButtons.size()) { qWarning() << "[GetLastTransactions] Transaction" << i + 1 << "not found after scrolling"; break; } } const Node &visibleBtn = txButtons[i]; TransactionInfo tx = xmlScreenParser.parseTransactionButtonText(visibleBtn.text); qDebug() << "[GetLastTransactions] Tapping transaction" << i + 1 << ":" << visibleBtn.text.left(50); AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y()); QThread::msleep(2000); // Ждём загрузки деталей — ищем дату в шапке TransactionInfo detail; for (int attempt = 0; attempt < 10; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); detail = xmlScreenParser.parseTransactionDetail(); if (!detail.bankTime.isEmpty()) break; // Если всё ещё на экране операций — тап не сработал, повторяем if (xmlScreenParser.isTransactionListScreen()) { qDebug() << "[GetLastTransactions] Still on transaction list, re-tapping" << i + 1; AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y()); } qDebug() << "[GetLastTransactions] Detail not loaded, attempt" << attempt + 1 << "/ 10"; QThread::msleep(1500); } tx.bankName = "ozon"; tx.bankTime = detail.bankTime; tx.status = TransactionStatus::Complete; tx.timestamp = DateUtils::getUtcNow(); if (!detail.name.isEmpty()) tx.name = detail.name; if (!detail.phone.isEmpty()) { // Оставляем только цифры QString cleanPhone; for (const QChar &ch : detail.phone) { if (ch.isDigit()) cleanPhone.append(ch); } tx.phone = cleanPhone; tx.type = TransactionType::Phone; } // Чистим bankTime от unicode символов: заменяем все не-ASCII пробелы на обычные if (!tx.bankTime.isEmpty()) { QString dtStr = tx.bankTime; for (int ci = 0; ci < dtStr.size(); ++ci) { const QChar ch = dtStr.at(ci); if (ch != ' ' && ch.category() == QChar::Separator_Space) { dtStr[ci] = ' '; } } tx.bankTime = dtStr; const QTimeZone msk("Europe/Moscow"); QDateTime parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm"); if (!parsed.isValid()) { parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm"); } if (parsed.isValid()) { parsed.setTimeZone(msk); tx.completeTime = parsed; } } qDebug() << "[GetLastTransactions]" << "date:" << tx.bankTime << "name:" << tx.description << "sender:" << tx.name << "phone:" << tx.phone << "amount:" << (tx.amount >= 0 ? "+" : "") + QString::number(tx.amount, 'f', 2); // Если задан лимит по количеству — проверяем счётчик // Иначе фильтруем по 24 часам if (m_maxCount > 0) { if (transactions.size() + 1 >= m_maxCount) { // Эта транзакция последняя — сохраним и выйдем после обработки } } else if (tx.completeTime.isValid()) { const qint64 ageSecs = tx.completeTime.toUTC().secsTo(QDateTime::currentDateTimeUtc()); if (ageSecs > 24 * 3600) { qDebug() << "[GetLastTransactions] Transaction older than 24h, stopping"; AdbUtils::goBack(m_deviceId); QThread::msleep(1000); break; } } // Сохраняем в БД если не существует // Берём первый аккаунт для этого устройства и приложения if (accountId != -1) { tx.accountId = accountId; if (TransactionDAO::insertTransactionIfNotExists(tx)) { qDebug() << "[GetLastTransactions] Saved transaction to DB"; // Делаем скриншот документа транзакции Node docBtn = xmlScreenParser.findButtonNode("Документы", false); if (!docBtn.isEmpty()) { qDebug() << "[GetLastTransactions] Tapping 'Документы'..."; AdbUtils::makeTap(m_deviceId, docBtn.x(), docBtn.y()); QThread::msleep(2000); // Ждём появления кнопки "Поделиться" bool docLoaded = false; for (int da = 0; da < 20; ++da) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); Node shareBtn = xmlScreenParser.findNodeByContentDesc("Поделиться"); if (!shareBtn.isEmpty()) { docLoaded = true; break; } qDebug() << "[GetLastTransactions] Waiting for 'Поделиться', attempt" << da + 1 << "/ 20"; QThread::msleep(1500); } if (docLoaded) { QThread::msleep(500); QProcess screencapProc; screencapProc.start("adb", QStringList() << "-s" << m_deviceId << "exec-out" << "screencap" << "-p"); screencapProc.waitForFinished(5000); const QByteArray screenshot = screencapProc.readAllStandardOutput(); if (!screenshot.isEmpty()) { TransactionInfo saved = TransactionDAO::findByBankTimeAndAmount( accountId, tx.bankName, tx.bankTime, tx.amount); if (saved.id != -1) { TransactionDAO::updateReceiptImage(saved.id, screenshot); qDebug() << "[GetLastTransactions] Receipt saved to DB, txId:" << saved.id; // OCR: распознаём текст и достаём ID операции const QString ocrText = recognizeTextFromImage(screenshot); if (!ocrText.isEmpty()) { qDebug() << "[GetLastTransactions] OCR result, txId:" << saved.id << "\n--- OCR TEXT START ---\n" << ocrText << "\n--- OCR TEXT END ---"; qDebug() << "[GetLastTransactions] OCR hex:" << ocrText.left(200).toUtf8().toHex(' '); const QString txId = extractTransactionId(ocrText); if (!txId.isEmpty()) { TransactionDAO::updateBankTrExternalId(saved.id, txId); qDebug() << "[GetLastTransactions] Extracted transaction ID:" << txId; } else { qWarning() << "[GetLastTransactions] Could not extract transaction ID from OCR text"; } } else { qWarning() << "[GetLastTransactions] OCR returned empty text"; } } } else { qWarning() << "[GetLastTransactions] Screenshot is empty"; } } else { qWarning() << "[GetLastTransactions] 'Поделиться' not found, skipping receipt"; } // Возвращаемся из документов в детали транзакции AdbUtils::goBack(m_deviceId); QThread::msleep(1000); } } else { qDebug() << "[GetLastTransactions] Transaction already exists or error"; } } transactions.append(tx); // Если достигнут лимит по количеству — выходим if (m_maxCount > 0 && transactions.size() >= m_maxCount) { qDebug() << "[GetLastTransactions] Reached maxCount limit:" << m_maxCount; AdbUtils::goBack(m_deviceId); QThread::msleep(1000); break; } // Возвращаемся в список AdbUtils::goBack(m_deviceId); QThread::msleep(1500); // Ждём возврата на экран списка for (int attempt = 0; attempt < 5; ++attempt) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); if (xmlScreenParser.isTransactionListScreen()) break; QThread::msleep(1000); } // Обновляем кнопки (после возврата координаты могут измениться) xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); txButtons = xmlScreenParser.findTransactionButtons(8); } emit finishedWithResult(m_error); } } // namespace Ozon