1
0
forked from BRT/arc
arc/android/ozon/GetLastTransactionsScript.cpp
slava e9aaa71c56 Add maxCount support to GetLastTransactionsScript and UI updates for transaction limits
Implemented a `maxCount` parameter in `GetLastTransactionsScript` to limit the number of transactions retrieved. Updated `AccountSettingsWindow` to include new transaction limit options in the UI. Enhanced retrieval logic to support either time-based filtering (24 hours) or count-based limits, ensuring flexibility in transaction handling.
2026-03-09 16:56:01 +07:00

393 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "GetLastTransactionsScript.h"
#include <QProcess>
#include <QRegularExpression>
#include <QThread>
#include <QTimeZone>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#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<const l_uint8 *>(pngData.constData()),
static_cast<size_t>(pngData.size()));
if (!pix) {
qWarning() << "[OCR] Failed to read image";
api->End();
delete api;
return {};
}
api->SetImage(pix);
char *text = api->GetUTF8Text();
QString result = QString::fromUtf8(text).trimmed();
delete[] text;
pixDestroy(&pix);
api->End();
delete api;
return result;
}
static QString extractTransactionId(const QString &ocrText) {
// "ID операции A60130542076330K0000040011670301" или "ID операции = Аб60650..."
QRegularExpression reId(R"(ID\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<Node> 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<AccountInfo> 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<TransactionInfo> 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;
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 ---";
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