Implemented `EventWindow` for viewing events with pagination and table-based UI. Added database migration to remove `UNIQUE` constraint on `external_id` in `events`. Enhanced DAO methods for event retrieval, pagination, and transaction updates. Integrated OCR recognition in transaction scripts and streamlined event handling in services.
368 lines
16 KiB
C++
368 lines
16 KiB
C++
#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"
|
||
QRegularExpression reId(R"(ID\s+операции\s+([A-Za-z0-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,
|
||
QObject *parent
|
||
) : CommonScript(parent),
|
||
m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)) {
|
||
}
|
||
|
||
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);
|
||
|
||
// Сохраняем в БД если не существует
|
||
// Берём первый аккаунт для этого устройства и приложения
|
||
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 < 10; ++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 << "/ 10";
|
||
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);
|
||
|
||
// Возвращаемся в список
|
||
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
|