Streamline FETCH_TRANSACTIONS to support per-transaction streaming, handle partial successes, and prevent duplicate retries. Update scripts, task lifecycles, and error reporting with multi-target processing.
This commit is contained in:
parent
3832a658ce
commit
709c7d0490
@ -249,15 +249,13 @@ namespace Dushanbe {
|
|||||||
GetLastTransactionsScript::GetLastTransactionsScript(
|
GetLastTransactionsScript::GetLastTransactionsScript(
|
||||||
QString deviceId,
|
QString deviceId,
|
||||||
QString appCode,
|
QString appCode,
|
||||||
double searchAmount,
|
QList<QPair<double, QDateTime>> targets,
|
||||||
QDateTime searchTime,
|
|
||||||
QString materialId,
|
QString materialId,
|
||||||
QObject *parent
|
QObject *parent
|
||||||
) : CommonScript(parent),
|
) : CommonScript(parent),
|
||||||
m_deviceId(std::move(deviceId)),
|
m_deviceId(std::move(deviceId)),
|
||||||
m_appCode(std::move(appCode)),
|
m_appCode(std::move(appCode)),
|
||||||
m_searchAmount(searchAmount),
|
m_targets(std::move(targets)),
|
||||||
m_searchTime(std::move(searchTime)),
|
|
||||||
m_materialId(std::move(materialId)) {
|
m_materialId(std::move(materialId)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +275,8 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskDumpMeta dumpMeta{m_materialId, m_searchAmount, {}, {}};
|
const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first;
|
||||||
|
const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}};
|
||||||
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
|
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
|
||||||
|
|
||||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
@ -378,9 +377,6 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скриншот списка транзакций — отправим при ошибке, удалим при успехе
|
|
||||||
const QByteArray listScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId);
|
|
||||||
|
|
||||||
// Находим accountId
|
// Находим accountId
|
||||||
int accountId = -1;
|
int accountId = -1;
|
||||||
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(device.id, m_appCode);
|
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(device.id, m_appCode);
|
||||||
@ -390,8 +386,62 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
qWarning() << "[Dushanbe::GetLastTransactions] No account found";
|
qWarning() << "[Dushanbe::GetLastTransactions] No account found";
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "[Dushanbe::GetLastTransactions] Searching: amount=" << m_searchAmount
|
// Ищем каждую цель из списка. Найденные отдаём сразу через transactionParsed;
|
||||||
<< "time=" << m_searchTime.toString(Qt::ISODate);
|
// ненайденные копим и репортим одной агрегированной ошибкой в конце.
|
||||||
|
QStringList notFound;
|
||||||
|
for (int i = 0; i < m_targets.size(); ++i) {
|
||||||
|
if (QThread::currentThread()->isInterruptionRequested()) { m_error = "Interrupted"; break; }
|
||||||
|
const double amount = m_targets[i].first;
|
||||||
|
const QDateTime time = m_targets[i].second;
|
||||||
|
// Перед каждой целью (кроме первой) возвращаемся к началу Выписки.
|
||||||
|
if (i > 0) scrollHistoryToTop(device);
|
||||||
|
if (!searchOne(device, accountId, amount, time)) {
|
||||||
|
if (m_error == "Interrupted") break;
|
||||||
|
notFound << QString("amount=%1 time=%2")
|
||||||
|
.arg(QString::number(amount, 'f', 2), time.toString(Qt::ISODate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_error != "Interrupted") {
|
||||||
|
m_error = notFound.isEmpty()
|
||||||
|
? QString()
|
||||||
|
: QString("Transactions not found (%1/%2): %3")
|
||||||
|
.arg(notFound.size()).arg(m_targets.size()).arg(notFound.join("; "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаемся на главную
|
||||||
|
{
|
||||||
|
Node glavnayaBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Главная"));
|
||||||
|
if (glavnayaBtn.x() > 0 && glavnayaBtn.y() > 0) {
|
||||||
|
AdbUtils::makeTap(m_deviceId, glavnayaBtn.x(), glavnayaBtn.y());
|
||||||
|
} else {
|
||||||
|
AdbUtils::goBack(m_deviceId);
|
||||||
|
}
|
||||||
|
QThread::msleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetLastTransactionsScript::scrollHistoryToTop(const DeviceInfo &device) {
|
||||||
|
// Свайпы сверху-вниз поднимают список Выписки к началу.
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
if (QThread::currentThread()->isInterruptionRequested()) return;
|
||||||
|
AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2,
|
||||||
|
device.screenHeight / 3,
|
||||||
|
device.screenWidth / 2,
|
||||||
|
device.screenHeight * 2 / 3, 300);
|
||||||
|
QThread::msleep(500);
|
||||||
|
}
|
||||||
|
QThread::msleep(800);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountId,
|
||||||
|
double searchAmount, const QDateTime &searchTime) {
|
||||||
|
// Скриншот списка транзакций — отправим при неудаче
|
||||||
|
const QByteArray listScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId);
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::GetLastTransactions] Searching: amount=" << searchAmount
|
||||||
|
<< "time=" << searchTime.toString(Qt::ISODate);
|
||||||
|
|
||||||
// Время в банковском приложении показывается в таймзоне Душанбе (UTC+5), не устройства
|
// Время в банковском приложении показывается в таймзоне Душанбе (UTC+5), не устройства
|
||||||
const QTimeZone tjTz("Asia/Dushanbe");
|
const QTimeZone tjTz("Asia/Dushanbe");
|
||||||
@ -412,8 +462,7 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
for (int scroll = 0; scroll <= maxScrolls; ++scroll) {
|
for (int scroll = 0; scroll <= maxScrolls; ++scroll) {
|
||||||
if (interrupted()) {
|
if (interrupted()) {
|
||||||
m_error = "Interrupted";
|
m_error = "Interrupted";
|
||||||
emit finishedWithResult(m_error);
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||||
@ -602,7 +651,7 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
<< "y=" << items[i].node.y();
|
<< "y=" << items[i].node.y();
|
||||||
}
|
}
|
||||||
|
|
||||||
const double searchAbs = qAbs(m_searchAmount);
|
const double searchAbs = qAbs(searchAmount);
|
||||||
for (const auto &tx : items) {
|
for (const auto &tx : items) {
|
||||||
const double absListAmt = qAbs(tx.amount);
|
const double absListAmt = qAbs(tx.amount);
|
||||||
const bool amountDirect = qAbs(absListAmt - searchAbs) < 0.01;
|
const bool amountDirect = qAbs(absListAmt - searchAbs) < 0.01;
|
||||||
@ -617,9 +666,9 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
// Pre-filter по ДАТЕ: только если дата надёжная (из content-desc P2P формата).
|
// Pre-filter по ДАТЕ: только если дата надёжная (из content-desc P2P формата).
|
||||||
// DCWallet транзакции получают дату из groupDates ImageView, которая
|
// DCWallet транзакции получают дату из groupDates ImageView, которая
|
||||||
// неправильно присваивается при скролле — не фильтруем по ней.
|
// неправильно присваивается при скролле — не фильтруем по ней.
|
||||||
if (tx.dateReliable && m_searchTime.isValid() && !tx.date.isEmpty()) {
|
if (tx.dateReliable && searchTime.isValid() && !tx.date.isEmpty()) {
|
||||||
const QDate listDate = QDate::fromString(tx.date, "dd.MM.yyyy");
|
const QDate listDate = QDate::fromString(tx.date, "dd.MM.yyyy");
|
||||||
const QDate searchLocalDate = m_searchTime.toTimeZone(tjTz).date();
|
const QDate searchLocalDate = searchTime.toTimeZone(tjTz).date();
|
||||||
if (listDate.isValid() && searchLocalDate.isValid()) {
|
if (listDate.isValid() && searchLocalDate.isValid()) {
|
||||||
const qint64 dayDiff = qAbs(listDate.daysTo(searchLocalDate));
|
const qint64 dayDiff = qAbs(listDate.daysTo(searchLocalDate));
|
||||||
if (dayDiff > 3) {
|
if (dayDiff > 3) {
|
||||||
@ -632,9 +681,9 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Проверка времени по HH:MM:SS (±10 минут, с учётом wraparound полуночи)
|
// Проверка времени по HH:MM:SS (±10 минут, с учётом wraparound полуночи)
|
||||||
if (m_searchTime.isValid() && !tx.time.isEmpty()) {
|
if (searchTime.isValid() && !tx.time.isEmpty()) {
|
||||||
const QTime listTime = QTime::fromString(tx.time, "HH:mm:ss");
|
const QTime listTime = QTime::fromString(tx.time, "HH:mm:ss");
|
||||||
const QTime searchLocalTime = m_searchTime.toTimeZone(tjTz).time();
|
const QTime searchLocalTime = searchTime.toTimeZone(tjTz).time();
|
||||||
if (listTime.isValid() && searchLocalTime.isValid()) {
|
if (listTime.isValid() && searchLocalTime.isValid()) {
|
||||||
const int diffSecs = qAbs(listTime.secsTo(searchLocalTime));
|
const int diffSecs = qAbs(listTime.secsTo(searchLocalTime));
|
||||||
if (diffSecs > 600 && diffSecs < 85800) { // 85800 = 23h50m
|
if (diffSecs > 600 && diffSecs < 85800) { // 85800 = 23h50m
|
||||||
@ -665,13 +714,13 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
const int tapX = txNode.x1() + 80;
|
const int tapX = txNode.x1() + 80;
|
||||||
AdbUtils::makeTap(m_deviceId, tapX, txNode.y());
|
AdbUtils::makeTap(m_deviceId, tapX, txNode.y());
|
||||||
QThread::msleep(3000);
|
QThread::msleep(3000);
|
||||||
if (interrupted()) { m_error = "Interrupted"; emit finishedWithResult(m_error); return; }
|
if (interrupted()) { m_error = "Interrupted"; return false; }
|
||||||
|
|
||||||
// Ждём экран деталей Выписки — ImageView с content-desc начинающимся с "Успешный платеж"
|
// Ждём экран деталей Выписки — ImageView с content-desc начинающимся с "Успешный платеж"
|
||||||
OcrTransactionDetail detail;
|
OcrTransactionDetail detail;
|
||||||
bool detailLoaded = false;
|
bool detailLoaded = false;
|
||||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
if (interrupted()) { m_error = "Interrupted"; emit finishedWithResult(m_error); return; }
|
if (interrupted()) { m_error = "Interrupted"; return false; }
|
||||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||||
// 1) Bottom-sheet формат (3.2.33, см. assets/dushanbe/3.2.33/tx_detail.xml):
|
// 1) Bottom-sheet формат (3.2.33, см. assets/dushanbe/3.2.33/tx_detail.xml):
|
||||||
@ -800,7 +849,7 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
<< "amount=" << detail.amount << "receiver=" << detail.receiverAccount;
|
<< "amount=" << detail.amount << "receiver=" << detail.receiverAccount;
|
||||||
|
|
||||||
// Проверяем время ±10 минут
|
// Проверяем время ±10 минут
|
||||||
if (m_searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) {
|
if (searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) {
|
||||||
QDateTime txTime = QDateTime::fromString(
|
QDateTime txTime = QDateTime::fromString(
|
||||||
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
|
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
|
||||||
if (!txTime.isValid()) {
|
if (!txTime.isValid()) {
|
||||||
@ -808,7 +857,7 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
}
|
}
|
||||||
if (txTime.isValid()) {
|
if (txTime.isValid()) {
|
||||||
txTime.setTimeZone(tjTz);
|
txTime.setTimeZone(tjTz);
|
||||||
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC()));
|
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(searchTime.toUTC()));
|
||||||
if (diffSecs > 600) {
|
if (diffSecs > 600) {
|
||||||
qDebug() << "[Dushanbe::GetLastTransactions] Time mismatch: diff=" << diffSecs << "sec (>10min), skipping";
|
qDebug() << "[Dushanbe::GetLastTransactions] Time mismatch: diff=" << diffSecs << "sec (>10min), skipping";
|
||||||
AdbUtils::goBack(m_deviceId);
|
AdbUtils::goBack(m_deviceId);
|
||||||
@ -877,13 +926,12 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
}
|
}
|
||||||
QThread::msleep(1000);
|
QThread::msleep(1000);
|
||||||
|
|
||||||
emit finishedWithResult(m_error); // m_error пустой — успех
|
return true; // успех — транзакция найдена и отдана через transactionParsed
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скроллим вниз — свайп внутри области транзакций
|
// Скроллим вниз — свайп внутри области транзакций
|
||||||
if (scroll < maxScrolls) {
|
if (scroll < maxScrolls) {
|
||||||
if (interrupted()) { m_error = "Interrupted"; emit finishedWithResult(m_error); return; }
|
if (interrupted()) { m_error = "Interrupted"; return false; }
|
||||||
const int x = device.screenWidth / 2;
|
const int x = device.screenWidth / 2;
|
||||||
// Определяем область контента из реальных Y позиций items
|
// Определяем область контента из реальных Y позиций items
|
||||||
int contentBottom = device.screenHeight * 2 / 3;
|
int contentBottom = device.screenHeight * 2 / 3;
|
||||||
@ -900,25 +948,18 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_error = "Transaction not found: amount=" + QString::number(m_searchAmount, 'f', 2)
|
// Эту цель не нашли. m_error не трогаем — агрегированную ошибку по всем
|
||||||
+ " time=" + m_searchTime.toString(Qt::ISODate);
|
// ненайденным целям соберёт оркестратор в doStart (он же вернёт на главную).
|
||||||
qWarning() << "[Dushanbe::GetLastTransactions]" << m_error;
|
qWarning() << "[Dushanbe::GetLastTransactions] Transaction not found: amount="
|
||||||
|
<< QString::number(searchAmount, 'f', 2)
|
||||||
|
<< "time=" << searchTime.toString(Qt::ISODate);
|
||||||
|
|
||||||
// Отправляем скриншот списка транзакций, сделанный до начала поиска
|
// Отправляем скриншот списка транзакций, сделанный до начала поиска
|
||||||
m_resultScreenshot = listScreenshot.isEmpty()
|
m_resultScreenshot = listScreenshot.isEmpty()
|
||||||
? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot;
|
? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot;
|
||||||
if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot);
|
if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot);
|
||||||
|
|
||||||
// Возвращаемся на главную
|
return false;
|
||||||
Node glavnayaBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Главная"));
|
|
||||||
if (glavnayaBtn.x() > 0 && glavnayaBtn.y() > 0) {
|
|
||||||
AdbUtils::makeTap(m_deviceId, glavnayaBtn.x(), glavnayaBtn.y());
|
|
||||||
} else {
|
|
||||||
AdbUtils::goBack(m_deviceId);
|
|
||||||
}
|
|
||||||
QThread::msleep(1000);
|
|
||||||
|
|
||||||
emit finishedWithResult(m_error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Dushanbe
|
} // namespace Dushanbe
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
#include "CommonScript.h"
|
#include "CommonScript.h"
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QPair>
|
||||||
#include "db/DeviceInfo.h"
|
#include "db/DeviceInfo.h"
|
||||||
#include "db/BankProfileInfo.h"
|
#include "db/BankProfileInfo.h"
|
||||||
|
|
||||||
@ -14,8 +16,7 @@ public:
|
|||||||
explicit GetLastTransactionsScript(
|
explicit GetLastTransactionsScript(
|
||||||
QString deviceId,
|
QString deviceId,
|
||||||
QString appCode,
|
QString appCode,
|
||||||
double searchAmount = 0,
|
QList<QPair<double, QDateTime>> targets = {},
|
||||||
QDateTime searchTime = {},
|
|
||||||
QString materialId = {},
|
QString materialId = {},
|
||||||
QObject *parent = nullptr
|
QObject *parent = nullptr
|
||||||
);
|
);
|
||||||
@ -23,7 +24,7 @@ public:
|
|||||||
~GetLastTransactionsScript() override;
|
~GetLastTransactionsScript() override;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
// Испускается, когда транзакция найдена и распарсена с экрана деталей.
|
// Испускается на КАЖДУЮ найденную транзакцию из списка целей.
|
||||||
// EventHandler использует эти данные напрямую для task_result вместо
|
// EventHandler использует эти данные напрямую для task_result вместо
|
||||||
// повторного чтения из БД.
|
// повторного чтения из БД.
|
||||||
void transactionParsed(const QJsonObject &tx);
|
void transactionParsed(const QJsonObject &tx);
|
||||||
@ -32,10 +33,19 @@ protected:
|
|||||||
void doStart() override;
|
void doStart() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Поиск одной цели по сумме и времени на экране "Выписка". Возвращает true
|
||||||
|
// и эмитит transactionParsed, если нашли; false — если нет. Прерывание
|
||||||
|
// выставляет m_error="Interrupted".
|
||||||
|
bool searchOne(const DeviceInfo &device, int accountId,
|
||||||
|
double searchAmount, const QDateTime &searchTime);
|
||||||
|
|
||||||
|
// Возврат к началу списка "Выписка" между поисками целей.
|
||||||
|
void scrollHistoryToTop(const DeviceInfo &device);
|
||||||
|
|
||||||
QString m_deviceId;
|
QString m_deviceId;
|
||||||
const QString m_appCode;
|
const QString m_appCode;
|
||||||
const double m_searchAmount; // сумма в сомони (0 = все последние)
|
// Список целей: пары (сумма в сомони со знаком, примерное время UTC).
|
||||||
const QDateTime m_searchTime; // примерное время (UTC)
|
const QList<QPair<double, QDateTime>> m_targets;
|
||||||
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
||||||
QString m_error;
|
QString m_error;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -179,6 +179,14 @@ bool CommonScript::runAppAndGoToHomeScreen(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем на онбординг-баннер "Обновили главную" поверх главной
|
||||||
|
if (Node onboardingNode = xmlScreenParser.tryToFindOnboardingBanner(); !onboardingNode.isEmpty()) {
|
||||||
|
qDebug() << "[runAppAndGoToHomeScreen] Onboarding banner detected, tapping 'Пропустить'...";
|
||||||
|
AdbUtils::makeTap(deviceId, onboardingNode.x(), onboardingNode.y());
|
||||||
|
QThread::msleep(1500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Проверяем, что это экран ввода пин-кода
|
// Проверяем, что это экран ввода пин-кода
|
||||||
if (!findAndInputPincode) {
|
if (!findAndInputPincode) {
|
||||||
const QList<Node> pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
const QList<Node> pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||||||
@ -225,6 +233,12 @@ bool CommonScript::tryToCloseAllBannersOnHomePage(const QString &deviceId, const
|
|||||||
closedAny = true;
|
closedAny = true;
|
||||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
continue;
|
continue;
|
||||||
|
} else if (Node onboardingNode = xmlScreenParser.tryToFindOnboardingBanner(); !onboardingNode.isEmpty()) {
|
||||||
|
// Онбординг "Обновили главную" поверх главной — жмём "Пропустить"
|
||||||
|
qDebug() << "[tryToCloseAllBannersOnHomePage] Onboarding banner detected, tapping 'Пропустить'...";
|
||||||
|
AdbUtils::makeTap(deviceId, onboardingNode.x(), onboardingNode.y());
|
||||||
|
QThread::msleep(1500);
|
||||||
|
closedThis = true;
|
||||||
} else if (Node closeBannerOrDialogNode = xmlScreenParser.tryToFindBannerOrDialog(width, height);
|
} else if (Node closeBannerOrDialogNode = xmlScreenParser.tryToFindBannerOrDialog(width, height);
|
||||||
closeBannerOrDialogNode.x() != -1 && closeBannerOrDialogNode.y() != -1) {
|
closeBannerOrDialogNode.x() != -1 && closeBannerOrDialogNode.y() != -1) {
|
||||||
AdbUtils::makeTap(deviceId, closeBannerOrDialogNode.x(), closeBannerOrDialogNode.y());
|
AdbUtils::makeTap(deviceId, closeBannerOrDialogNode.x(), closeBannerOrDialogNode.y());
|
||||||
|
|||||||
@ -82,16 +82,14 @@ GetLastTransactionsScript::GetLastTransactionsScript(
|
|||||||
QString deviceId,
|
QString deviceId,
|
||||||
QString appCode,
|
QString appCode,
|
||||||
int maxCount,
|
int maxCount,
|
||||||
double searchAmount,
|
QList<QPair<double, QDateTime>> targets,
|
||||||
QDateTime searchTime,
|
|
||||||
QString materialId,
|
QString materialId,
|
||||||
QObject *parent
|
QObject *parent
|
||||||
) : CommonScript(parent),
|
) : CommonScript(parent),
|
||||||
m_deviceId(std::move(deviceId)),
|
m_deviceId(std::move(deviceId)),
|
||||||
m_appCode(std::move(appCode)),
|
m_appCode(std::move(appCode)),
|
||||||
m_maxCount(maxCount),
|
m_maxCount(maxCount),
|
||||||
m_searchAmount(searchAmount),
|
m_targets(std::move(targets)),
|
||||||
m_searchTime(std::move(searchTime)),
|
|
||||||
m_materialId(std::move(materialId)) {
|
m_materialId(std::move(materialId)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +108,8 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskDumpMeta dumpMeta{m_materialId, m_searchAmount, {}, {}};
|
const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first;
|
||||||
|
const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}};
|
||||||
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
|
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
|
||||||
|
|
||||||
if (!navigateToTransactionList(device, app)) {
|
if (!navigateToTransactionList(device, app)) {
|
||||||
@ -127,9 +126,31 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
qWarning() << "[GetLastTransactions] No account found for" << device.id << m_appCode;
|
qWarning() << "[GetLastTransactions] No account found for" << device.id << m_appCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Выбор режима: поиск конкретной транзакции или сканирование последних
|
// Выбор режима: поиск списка конкретных транзакций или сканирование последних.
|
||||||
if (qAbs(m_searchAmount) > 0.01) {
|
if (!m_targets.isEmpty()) {
|
||||||
searchTransaction(device, accountId);
|
// Ищем каждую цель из списка. Найденные отдаём сразу через
|
||||||
|
// transactionParsed; ненайденные копим и репортим одной ошибкой в конце.
|
||||||
|
QStringList notFound;
|
||||||
|
for (int i = 0; i < m_targets.size(); ++i) {
|
||||||
|
if (QThread::currentThread()->isInterruptionRequested()) {
|
||||||
|
m_error = "Interrupted";
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const double amount = m_targets[i].first;
|
||||||
|
const QDateTime time = m_targets[i].second;
|
||||||
|
// Перед каждой целью (кроме первой) возвращаемся к началу списка,
|
||||||
|
// т.к. предыдущий поиск мог проскроллить вниз к другой дате.
|
||||||
|
if (i > 0) scrollListToTop(device);
|
||||||
|
if (!searchOne(device, accountId, amount, time)) {
|
||||||
|
notFound << QString("amount=%1 time=%2")
|
||||||
|
.arg(QString::number(amount, 'f', 2), time.toString(Qt::ISODate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_error = notFound.isEmpty()
|
||||||
|
? QString()
|
||||||
|
: QString("Transactions not found (%1/%2): %3")
|
||||||
|
.arg(notFound.size()).arg(m_targets.size()).arg(notFound.join("; "));
|
||||||
} else {
|
} else {
|
||||||
scanRecentTransactions(device, accountId);
|
scanRecentTransactions(device, accountId);
|
||||||
}
|
}
|
||||||
@ -242,8 +263,9 @@ bool GetLastTransactionsScript::navigateToTransactionList(const DeviceInfo &devi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int accountId) {
|
bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountId,
|
||||||
// Скриншот списка транзакций — отправим при ошибке, удалим при успехе
|
double searchAmount, const QDateTime &searchTime) {
|
||||||
|
// Скриншот списка транзакций — отправим при неудаче, удалим при успехе
|
||||||
const QByteArray listScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId);
|
const QByteArray listScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId);
|
||||||
|
|
||||||
// Время в Ozon показывается в таймзоне устройства, а не в MSK
|
// Время в Ozon показывается в таймзоне устройства, а не в MSK
|
||||||
@ -266,12 +288,12 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
|||||||
int navBarTop = detectNavBarTop();
|
int navBarTop = detectNavBarTop();
|
||||||
|
|
||||||
// Целевая дата в таймзоне устройства для поиска заголовка дня в списке
|
// Целевая дата в таймзоне устройства для поиска заголовка дня в списке
|
||||||
const QDateTime searchLocal = m_searchTime.toTimeZone(deviceTz);
|
const QDateTime searchLocal = searchTime.toTimeZone(deviceTz);
|
||||||
const QDate searchDate = searchLocal.date();
|
const QDate searchDate = searchLocal.date();
|
||||||
|
|
||||||
qDebug() << "[FetchTransactions] Searching: device=" << m_deviceId
|
qDebug() << "[FetchTransactions] Searching: device=" << m_deviceId
|
||||||
<< "amount=" << m_searchAmount
|
<< "amount=" << searchAmount
|
||||||
<< "time=" << m_searchTime.toString(Qt::ISODate)
|
<< "time=" << searchTime.toString(Qt::ISODate)
|
||||||
<< "tz=" << tzName
|
<< "tz=" << tzName
|
||||||
<< "date(local)=" << searchDate.toString("dd.MM.yyyy")
|
<< "date(local)=" << searchDate.toString("dd.MM.yyyy")
|
||||||
<< "screen=" << device.screenWidth << "x" << device.screenHeight;
|
<< "screen=" << device.screenWidth << "x" << device.screenHeight;
|
||||||
@ -348,7 +370,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
|||||||
for (int scroll = 0; scroll < maxScrolls; ++scroll) {
|
for (int scroll = 0; scroll < maxScrolls; ++scroll) {
|
||||||
if (QThread::currentThread()->isInterruptionRequested()) {
|
if (QThread::currentThread()->isInterruptionRequested()) {
|
||||||
m_error = "Interrupted";
|
m_error = "Interrupted";
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||||
@ -369,12 +391,12 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!dateFound) {
|
if (!dateFound) {
|
||||||
m_error = "Date not found in transaction list: " + searchDate.toString("dd.MM.yyyy");
|
qWarning() << "[FetchTransactions] Date not found in transaction list: "
|
||||||
qWarning() << "[FetchTransactions]" << m_error;
|
<< searchDate.toString("dd.MM.yyyy");
|
||||||
m_resultScreenshot = listScreenshot.isEmpty()
|
m_resultScreenshot = listScreenshot.isEmpty()
|
||||||
? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot;
|
? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot;
|
||||||
if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot);
|
if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Хелпер: находит Y-диапазон [dateY, nextDateY) для нужной даты на экране.
|
// Хелпер: находит Y-диапазон [dateY, nextDateY) для нужной даты на экране.
|
||||||
@ -436,7 +458,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
|||||||
for (int attempt = 0; attempt < 15; ++attempt) {
|
for (int attempt = 0; attempt < 15; ++attempt) {
|
||||||
if (QThread::currentThread()->isInterruptionRequested()) {
|
if (QThread::currentThread()->isInterruptionRequested()) {
|
||||||
m_error = "Interrupted";
|
m_error = "Interrupted";
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||||
@ -503,7 +525,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
|||||||
for (int bi = 0; bi < txButtons.size(); ++bi) {
|
for (int bi = 0; bi < txButtons.size(); ++bi) {
|
||||||
if (QThread::currentThread()->isInterruptionRequested()) {
|
if (QThread::currentThread()->isInterruptionRequested()) {
|
||||||
m_error = "Interrupted";
|
m_error = "Interrupted";
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пропускаем кнопки вне Y-диапазона нужной даты.
|
// Пропускаем кнопки вне Y-диапазона нужной даты.
|
||||||
@ -517,9 +539,9 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
|||||||
|
|
||||||
const TransactionInfo parsed = xmlScreenParser.parseTransactionButtonText(txButtons[bi].text);
|
const TransactionInfo parsed = xmlScreenParser.parseTransactionButtonText(txButtons[bi].text);
|
||||||
qDebug() << "[FetchTransactions] Button" << bi << "y=" << btnY
|
qDebug() << "[FetchTransactions] Button" << bi << "y=" << btnY
|
||||||
<< "parsed amount=" << parsed.amount << "search=" << m_searchAmount
|
<< "parsed amount=" << parsed.amount << "search=" << searchAmount
|
||||||
<< "text=" << txButtons[bi].text.left(60);
|
<< "text=" << txButtons[bi].text.left(60);
|
||||||
if (qAbs(parsed.amount - m_searchAmount) > 0.01) continue;
|
if (qAbs(parsed.amount - searchAmount) > 0.01) continue;
|
||||||
|
|
||||||
const Node &visibleBtn = txButtons[bi];
|
const Node &visibleBtn = txButtons[bi];
|
||||||
// Зажимаем тап-Y на видимую часть кнопки: если кнопка заходит под
|
// Зажимаем тап-Y на видимую часть кнопки: если кнопка заходит под
|
||||||
@ -543,7 +565,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
|||||||
for (int da = 0; da < 10; ++da) {
|
for (int da = 0; da < 10; ++da) {
|
||||||
if (QThread::currentThread()->isInterruptionRequested()) {
|
if (QThread::currentThread()->isInterruptionRequested()) {
|
||||||
m_error = "Interrupted";
|
m_error = "Interrupted";
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||||
@ -597,8 +619,8 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем время (±10 минут)
|
// Проверяем время (±10 минут)
|
||||||
if (txTime.isValid() && m_searchTime.isValid()) {
|
if (txTime.isValid() && searchTime.isValid()) {
|
||||||
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC()));
|
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(searchTime.toUTC()));
|
||||||
if (diffSecs > 600) {
|
if (diffSecs > 600) {
|
||||||
qDebug() << "[FetchTransactions] Time mismatch:" << dtStr
|
qDebug() << "[FetchTransactions] Time mismatch:" << dtStr
|
||||||
<< "expected" << searchLocal.toString("dd.MM.yyyy, HH:mm")
|
<< "expected" << searchLocal.toString("dd.MM.yyyy, HH:mm")
|
||||||
@ -680,7 +702,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
|||||||
QThread::msleep(1000);
|
QThread::msleep(1000);
|
||||||
AdbUtils::goBack(m_deviceId);
|
AdbUtils::goBack(m_deviceId);
|
||||||
QThread::msleep(1000);
|
QThread::msleep(1000);
|
||||||
return; // успех
|
return true; // успех
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отслеживаем прогресс: "без прогресса" только если были кнопки с СОВПАДАЮЩЕЙ
|
// Отслеживаем прогресс: "без прогресса" только если были кнопки с СОВПАДАЮЩЕЙ
|
||||||
@ -751,14 +773,30 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_error = "Transaction not found: device=" + m_deviceId
|
// Эту цель не нашли. m_error не трогаем — агрегированную ошибку по всем
|
||||||
+ " amount=" + QString::number(m_searchAmount, 'f', 2)
|
// ненайденным целям соберёт оркестратор в doStart.
|
||||||
+ " date=" + searchDate.toString("dd.MM.yyyy");
|
qWarning() << "[FetchTransactions] Transaction not found: device=" << m_deviceId
|
||||||
qWarning() << "[FetchTransactions]" << m_error;
|
<< "amount=" << QString::number(searchAmount, 'f', 2)
|
||||||
|
<< "date=" << searchDate.toString("dd.MM.yyyy");
|
||||||
// Отправляем скриншот списка транзакций, сделанный до начала поиска
|
// Отправляем скриншот списка транзакций, сделанный до начала поиска
|
||||||
m_resultScreenshot = listScreenshot.isEmpty()
|
m_resultScreenshot = listScreenshot.isEmpty()
|
||||||
? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot;
|
? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot;
|
||||||
if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot);
|
if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetLastTransactionsScript::scrollListToTop(const DeviceInfo &device) {
|
||||||
|
// Несколько быстрых свайпов вверх, чтобы вернуться к началу списка операций.
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
if (QThread::currentThread()->isInterruptionRequested()) return;
|
||||||
|
AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2,
|
||||||
|
device.screenHeight / 3,
|
||||||
|
device.screenWidth / 2,
|
||||||
|
device.screenHeight * 2 / 3, 200);
|
||||||
|
QThread::msleep(400);
|
||||||
|
}
|
||||||
|
QThread::msleep(800);
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetLastTransactionsScript::scanRecentTransactions(const DeviceInfo &device, int accountId) {
|
void GetLastTransactionsScript::scanRecentTransactions(const DeviceInfo &device, int accountId) {
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
#include "CommonScript.h"
|
#include "CommonScript.h"
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QPair>
|
||||||
#include "db/DeviceInfo.h"
|
#include "db/DeviceInfo.h"
|
||||||
#include "db/BankProfileInfo.h"
|
#include "db/BankProfileInfo.h"
|
||||||
|
|
||||||
@ -11,6 +13,7 @@ class GetLastTransactionsScript final : public CommonScript {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
// Испускается на КАЖДУЮ найденную транзакцию из списка целей.
|
||||||
void transactionParsed(const QJsonObject &tx);
|
void transactionParsed(const QJsonObject &tx);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@ -18,8 +21,7 @@ public:
|
|||||||
QString deviceId,
|
QString deviceId,
|
||||||
QString appCode,
|
QString appCode,
|
||||||
int maxCount = 0,
|
int maxCount = 0,
|
||||||
double searchAmount = 0,
|
QList<QPair<double, QDateTime>> targets = {},
|
||||||
QDateTime searchTime = {},
|
|
||||||
QString materialId = {},
|
QString materialId = {},
|
||||||
QObject *parent = nullptr
|
QObject *parent = nullptr
|
||||||
);
|
);
|
||||||
@ -36,14 +38,20 @@ private:
|
|||||||
// Сканирование последних транзакций (для UI: 24ч или maxCount)
|
// Сканирование последних транзакций (для UI: 24ч или maxCount)
|
||||||
void scanRecentTransactions(const DeviceInfo &device, int accountId);
|
void scanRecentTransactions(const DeviceInfo &device, int accountId);
|
||||||
|
|
||||||
// Поиск конкретной транзакции по сумме и времени (для задач FETCH_TRANSACTIONS)
|
// Поиск одной конкретной транзакции по сумме и времени (FETCH_TRANSACTIONS).
|
||||||
void searchTransaction(const DeviceInfo &device, int accountId);
|
// Возвращает true и эмитит transactionParsed, если нашли; false — если нет.
|
||||||
|
bool searchOne(const DeviceInfo &device, int accountId,
|
||||||
|
double searchAmount, const QDateTime &searchTime);
|
||||||
|
|
||||||
|
// Быстрый возврат к началу списка операций (между поисками целей)
|
||||||
|
void scrollListToTop(const DeviceInfo &device);
|
||||||
|
|
||||||
QString m_deviceId;
|
QString m_deviceId;
|
||||||
const QString m_appCode;
|
const QString m_appCode;
|
||||||
const int m_maxCount; // 0 = фильтр по 24ч, >0 = N последних
|
const int m_maxCount; // 0 = фильтр по 24ч, >0 = N последних
|
||||||
const double m_searchAmount; // сумма в рублях (0 = не искать конкретную)
|
// Список целей поиска: пары (сумма в рублях со знаком, примерное время UTC).
|
||||||
const QDateTime m_searchTime; // примерное время (UTC)
|
// Пустой список → режим сканирования последних (scanRecentTransactions).
|
||||||
|
const QList<QPair<double, QDateTime>> m_targets;
|
||||||
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
||||||
QString m_error;
|
QString m_error;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -323,6 +323,28 @@ Node ScreenXmlParser::tryToFindGooglePlayBanner(const int screenWidth, const int
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Node ScreenXmlParser::tryToFindOnboardingBanner() {
|
||||||
|
// Онбординг "Обновили главную" рендерится во ViewPager с этим resource-id.
|
||||||
|
// Кнопка "Пропустить" — TextView (text_atom) внутри кликабельного контейнера,
|
||||||
|
// занимающего всю ширину верхней полоски. Сам TextView некликабельный, но его
|
||||||
|
// bounds совпадают с кликабельным родителем, поэтому тап по его центру попадает.
|
||||||
|
bool hasOnboarding = false;
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.resourceId == "ru.ozon.fintech.finance:id/onboarding_viewpager") {
|
||||||
|
hasOnboarding = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasOnboarding) return {};
|
||||||
|
|
||||||
|
for (const Node &node : m_nodes) {
|
||||||
|
if (node.text.trimmed() == QString::fromUtf8("Пропустить")) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
bool ScreenXmlParser::isProfileScreen() {
|
bool ScreenXmlParser::isProfileScreen() {
|
||||||
// Страница профиля Ozon: есть заголовок "Аккаунт" и кнопка "Выйти"
|
// Страница профиля Ozon: есть заголовок "Аккаунт" и кнопка "Выйти"
|
||||||
bool hasTitle = false;
|
bool hasTitle = false;
|
||||||
|
|||||||
@ -50,6 +50,11 @@ public:
|
|||||||
|
|
||||||
Node tryToFindGooglePlayBanner(int screenWidth, int screenHeight);
|
Node tryToFindGooglePlayBanner(int screenWidth, int screenHeight);
|
||||||
|
|
||||||
|
// Онбординг-баннер "Обновили главную" (resource-id onboarding_viewpager),
|
||||||
|
// появляется поверх главного экрана Ozon. Возвращает кнопку "Пропустить"
|
||||||
|
// для тапа, либо пустой Node если баннер не виден.
|
||||||
|
Node tryToFindOnboardingBanner();
|
||||||
|
|
||||||
// Находит кнопку "ЗАПРЕТИТЬ" в системном диалоге запроса разрешения Android
|
// Находит кнопку "ЗАПРЕТИТЬ" в системном диалоге запроса разрешения Android
|
||||||
// (com.google.android.permissioncontroller). Возвращает пустой Node если диалог не виден.
|
// (com.google.android.permissioncontroller). Возвращает пустой Node если диалог не виден.
|
||||||
Node tryToFindSystemPermissionDenyButton();
|
Node tryToFindSystemPermissionDenyButton();
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
#include "dushanbe/PayByCardScript.h"
|
#include "dushanbe/PayByCardScript.h"
|
||||||
#include "dushanbe/PayByPhoneScript.h"
|
#include "dushanbe/PayByPhoneScript.h"
|
||||||
|
|
||||||
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data, int eventId = -1);
|
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data, int eventId = -1, bool updateStatus = true);
|
||||||
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description, const QByteArray &screenshot = {}, int eventId = -1, const QString &transactionId = {});
|
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description, const QByteArray &screenshot = {}, int eventId = -1, const QString &transactionId = {});
|
||||||
static void sendMonitoringLog(const QString &type, const QString &event, const QString &message, const QByteArray &image = {}, int eventId = -1, bool toApiBase = false);
|
static void sendMonitoringLog(const QString &type, const QString &event, const QString &message, const QByteArray &image = {}, int eventId = -1, bool toApiBase = false);
|
||||||
|
|
||||||
@ -175,7 +175,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName,
|
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName,
|
||||||
const QJsonValue &data, int eventId) {
|
const QJsonValue &data, int eventId, bool updateStatus) {
|
||||||
QJsonObject obj;
|
QJsonObject obj;
|
||||||
obj["type"] = type;
|
obj["type"] = type;
|
||||||
obj["material_id"] = materialId; // UUID string
|
obj["material_id"] = materialId; // UUID string
|
||||||
@ -190,8 +190,11 @@ static void sendTaskResult(const QString &type, const QString &materialId, const
|
|||||||
networkService->setPayload(QJsonDocument(obj).toJson());
|
networkService->setPayload(QJsonDocument(obj).toJson());
|
||||||
QObject::connect(thread, &QThread::started, networkService, &NetworkService::postTaskResult);
|
QObject::connect(thread, &QThread::started, networkService, &NetworkService::postTaskResult);
|
||||||
QObject::connect(networkService, &NetworkService::finishedWithResult, networkService,
|
QObject::connect(networkService, &NetworkService::finishedWithResult, networkService,
|
||||||
[eventId](int result) {
|
[eventId, updateStatus](int result) {
|
||||||
if (eventId != -1) {
|
// updateStatus=false для потоковых per-tx результатов FETCH_TRANSACTIONS:
|
||||||
|
// статус события финализируется один раз в updateEventThread, а не на
|
||||||
|
// каждой отдельной отправке найденной транзакции.
|
||||||
|
if (eventId != -1 && updateStatus) {
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
EventDAO::markSentToServer(eventId);
|
EventDAO::markSentToServer(eventId);
|
||||||
} else {
|
} else {
|
||||||
@ -288,9 +291,11 @@ bool EventHandler::tryRetry(const QString &key, const EventInfo &event, const QS
|
|||||||
.arg(QString::number(attempt), QString::number(kMaxRetries),
|
.arg(QString::number(attempt), QString::number(kMaxRetries),
|
||||||
event.deviceId, event.externalId, reason));
|
event.deviceId, event.externalId, reason));
|
||||||
|
|
||||||
// Чистим артефакты прошлой попытки
|
// Чистим артефакты прошлой попытки. Ретрай запускается только когда ещё
|
||||||
|
// ничего не отправлено (см. guard в updateEventThread), так что сбрасываем
|
||||||
|
// счётчик отправленных в 0.
|
||||||
m_screenshots.remove(key);
|
m_screenshots.remove(key);
|
||||||
m_parsedTxs.remove(key);
|
m_parsedTxCount.remove(key);
|
||||||
|
|
||||||
// Снимаем текущий поток/таймер (в timeout-ветке они уже сняты — take() вернёт null)
|
// Снимаем текущий поток/таймер (в timeout-ветке они уже сняты — take() вернёт null)
|
||||||
if (const auto tm = m_timers.take(key)) {
|
if (const auto tm = m_timers.take(key)) {
|
||||||
@ -317,8 +322,12 @@ bool EventHandler::tryRetry(const QString &key, const EventInfo &event, const QS
|
|||||||
void EventHandler::updateEventThread(const QString &key, const EventInfo &event, const QString &result) {
|
void EventHandler::updateEventThread(const QString &key, const EventInfo &event, const QString &result) {
|
||||||
|
|
||||||
// Поиск транзакции: при ошибке скрипта перезапускаем (ещё до 2 раз) вместо
|
// Поиск транзакции: при ошибке скрипта перезапускаем (ещё до 2 раз) вместо
|
||||||
// немедленной отправки ошибки на сервер.
|
// немедленной отправки ошибки на сервер. Но только если НИ ОДНОЙ транзакции
|
||||||
|
// ещё не отправлено: при частичном успехе (часть списка найдена и уже
|
||||||
|
// улетела на сервер) ретрай бессмыслен — переотправит дубли, поэтому
|
||||||
|
// ненайденные просто репортим ошибкой ниже.
|
||||||
if (!result.isEmpty() && event.type == EventType::FetchTransactions
|
if (!result.isEmpty() && event.type == EventType::FetchTransactions
|
||||||
|
&& m_parsedTxCount.value(key, 0) == 0
|
||||||
&& tryRetry(key, event, result)) {
|
&& tryRetry(key, event, result)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -413,24 +422,13 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
obj["currency"] = currencyCode;
|
obj["currency"] = currencyCode;
|
||||||
taskData = obj;
|
taskData = obj;
|
||||||
} else if (event.type == EventType::FetchTransactions) {
|
} else if (event.type == EventType::FetchTransactions) {
|
||||||
// FETCH_TRANSACTIONS больше НЕ читает из БД — берём то, что
|
// FETCH_TRANSACTIONS шлёт результаты потоково: каждая найденная
|
||||||
// скрипт распарсил вживую и передал через сигнал transactionParsed.
|
// транзакция уже отправлена отдельным task_result по сигналу
|
||||||
const QJsonObject parsedTx = m_parsedTxs.take(key);
|
// transactionParsed (см. startThreadForTask). Здесь, при полном
|
||||||
if (parsedTx.isEmpty()) {
|
// успехе (m_error пуст → найдены все цели списка), отдельный
|
||||||
qWarning() << "[EventHandler] FETCH_TRANSACTIONS: script did not emit parsed tx";
|
// финальный task_result НЕ отправляем — только финализируем
|
||||||
if (tryRetry(key, event, "Transaction not parsed from screen")) return;
|
// событие, чтобы оно не считалось «зависшим» при перезапуске.
|
||||||
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName,
|
|
||||||
"Transaction not parsed from screen", {}, -1, event.transactionId);
|
|
||||||
EventDAO::updateEvent(event.id, EventStatus::Error,
|
|
||||||
"Transaction not parsed from screen");
|
|
||||||
EventDAO::markSentToServer(event.id);
|
EventDAO::markSentToServer(event.id);
|
||||||
m_threads.remove(key);
|
|
||||||
m_retryCount.remove(event.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QJsonArray arr;
|
|
||||||
arr.append(parsedTx);
|
|
||||||
taskData = arr;
|
|
||||||
} else if (event.type == EventType::CreateTransaction) {
|
} else if (event.type == EventType::CreateTransaction) {
|
||||||
// Определяем тип транзакции из JSON задачи
|
// Определяем тип транзакции из JSON задачи
|
||||||
const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object();
|
const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object();
|
||||||
@ -467,6 +465,9 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
obj["status"] = "completed";
|
obj["status"] = "completed";
|
||||||
taskData = obj;
|
taskData = obj;
|
||||||
}
|
}
|
||||||
|
// FETCH_TRANSACTIONS уже отправил данные потоково (per-tx) —
|
||||||
|
// финальный агрегирующий результат не нужен.
|
||||||
|
if (event.type != EventType::FetchTransactions) {
|
||||||
sendTaskResult(
|
sendTaskResult(
|
||||||
eventTypeToString(event.type),
|
eventTypeToString(event.type),
|
||||||
event.externalId,
|
event.externalId,
|
||||||
@ -475,6 +476,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
event.id
|
event.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (newStatus == EventStatus::Complete) {
|
if (newStatus == EventStatus::Complete) {
|
||||||
const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId);
|
const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId);
|
||||||
@ -560,6 +562,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
}
|
}
|
||||||
m_threads.remove(key);
|
m_threads.remove(key);
|
||||||
m_retryCount.remove(event.id);
|
m_retryCount.remove(event.id);
|
||||||
|
m_parsedTxCount.remove(key);
|
||||||
|
|
||||||
// Убиваем банковское приложение на устройстве после завершения задачи
|
// Убиваем банковское приложение на устройстве после завершения задачи
|
||||||
{
|
{
|
||||||
@ -628,36 +631,54 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == EventType::FetchTransactions) {
|
if (event.type == EventType::FetchTransactions) {
|
||||||
// Парсим amount и created_at из body задачи
|
// body теперь — массив транзакций для поиска: [{amount, created_at}, ...].
|
||||||
|
// Для обратной совместимости поддерживаем и старый формат — одиночный
|
||||||
|
// объект {amount, created_at} (оборачиваем в массив из одного элемента).
|
||||||
const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object();
|
const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object();
|
||||||
const QJsonObject body = taskJson["body"].toObject();
|
QJsonArray bodyArr = taskJson["body"].toArray();
|
||||||
const double searchAmount = body["amount"].toDouble() / 100.0; // копейки → рубли (с учётом знака)
|
if (bodyArr.isEmpty() && taskJson["body"].isObject())
|
||||||
const QString createdAtStr = body["created_at"].toString();
|
bodyArr.append(taskJson["body"]);
|
||||||
QDateTime searchTime = QDateTime::fromString(createdAtStr, Qt::ISODate);
|
|
||||||
// Backend контракт — UTC ISO с суффиксом 'Z' (как мы сами пишем
|
QList<QPair<double, QDateTime>> targets;
|
||||||
// на строках 317 и 373). Если 'Z'/смещение отсутствует — Qt парсит
|
for (const auto &v : bodyArr) {
|
||||||
// как LocalTime, что даёт off-by-one для вечерних транзакций
|
const QJsonObject item = v.toObject();
|
||||||
// (см. task_dump (26): Ozon группировал +100 на 14.05, backend
|
const double searchAmount = item["amount"].toDouble() / 100.0; // копейки → рубли (с учётом знака)
|
||||||
// прислал 13.05 без TZ). Жёстко переинтерпретируем как UTC.
|
QDateTime searchTime = QDateTime::fromString(item["created_at"].toString(), Qt::ISODate);
|
||||||
|
// Backend контракт — UTC ISO с суффиксом 'Z'. Если 'Z'/смещение
|
||||||
|
// отсутствует — Qt парсит как LocalTime, что даёт off-by-one для
|
||||||
|
// вечерних транзакций (см. task_dump (26)). Жёстко интерпретируем как UTC.
|
||||||
if (searchTime.isValid() && searchTime.timeSpec() == Qt::LocalTime) {
|
if (searchTime.isValid() && searchTime.timeSpec() == Qt::LocalTime) {
|
||||||
qWarning() << "[EventHandler] FETCH_TRANSACTIONS: created_at без TZ-маркера"
|
qWarning() << "[EventHandler] FETCH_TRANSACTIONS: created_at без TZ-маркера"
|
||||||
<< createdAtStr << "— интерпретирую как UTC";
|
<< item["created_at"].toString() << "— интерпретирую как UTC";
|
||||||
searchTime.setTimeZone(QTimeZone::utc());
|
searchTime.setTimeZone(QTimeZone::utc());
|
||||||
}
|
}
|
||||||
|
targets.append({searchAmount, searchTime});
|
||||||
|
}
|
||||||
|
qDebug() << "[EventHandler] FETCH_TRANSACTIONS: searching" << targets.size() << "transaction(s)";
|
||||||
|
|
||||||
|
// Потоковая отправка: каждую найденную транзакцию шлём на сервер сразу
|
||||||
|
// отдельным task_result (updateStatus=false — статус события финализирует
|
||||||
|
// updateEventThread). m_parsedTxCount считает отправленные, чтобы не
|
||||||
|
// ретраить уже частично отработанную задачу.
|
||||||
|
auto streamParsedTx = [this, key, event](const QJsonObject &tx) {
|
||||||
|
m_parsedTxCount[key] = m_parsedTxCount.value(key, 0) + 1;
|
||||||
|
QJsonArray arr;
|
||||||
|
arr.append(tx);
|
||||||
|
sendTaskResult(eventTypeToString(EventType::FetchTransactions),
|
||||||
|
event.externalId, event.bankName, arr, event.id, /*updateStatus=*/false);
|
||||||
|
};
|
||||||
|
|
||||||
if (appCode == "ozon") {
|
if (appCode == "ozon") {
|
||||||
auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime, account.materialId);
|
auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, targets, account.materialId);
|
||||||
setup(w); connectScreenshot(w);
|
setup(w); connectScreenshot(w);
|
||||||
connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this,
|
connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this, streamParsedTx);
|
||||||
[this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; });
|
|
||||||
}
|
}
|
||||||
else if (appCode == "black")
|
else if (appCode == "black")
|
||||||
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
|
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
|
||||||
else if (appCode == "dushanbe_city_bank") {
|
else if (appCode == "dushanbe_city_bank") {
|
||||||
auto *w = new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, searchAmount, searchTime, account.materialId);
|
auto *w = new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, targets, account.materialId);
|
||||||
setup(w); connectScreenshotDushanbe(w);
|
setup(w); connectScreenshotDushanbe(w);
|
||||||
connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this,
|
connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this, streamParsedTx);
|
||||||
[this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; });
|
|
||||||
}
|
}
|
||||||
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
||||||
}
|
}
|
||||||
@ -712,8 +733,10 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
|||||||
}
|
}
|
||||||
m_threads.remove(key);
|
m_threads.remove(key);
|
||||||
|
|
||||||
// Поиск транзакции: таймаут тоже ретраим (ещё до 2 раз)
|
// Поиск транзакции: таймаут тоже ретраим (ещё до 2 раз), но только если
|
||||||
|
// ещё ни одной транзакции не отправлено (иначе ретрай переотправит дубли).
|
||||||
if (event.type == EventType::FetchTransactions
|
if (event.type == EventType::FetchTransactions
|
||||||
|
&& m_parsedTxCount.value(key, 0) == 0
|
||||||
&& tryRetry(key, event, "Script timeout (5 min)")) {
|
&& tryRetry(key, event, "Script timeout (5 min)")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -812,7 +835,7 @@ void EventHandler::clearAll() {
|
|||||||
}
|
}
|
||||||
m_timers.clear();
|
m_timers.clear();
|
||||||
m_screenshots.clear();
|
m_screenshots.clear();
|
||||||
m_parsedTxs.clear();
|
m_parsedTxCount.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EventHandler::stop() {
|
void EventHandler::stop() {
|
||||||
|
|||||||
@ -34,7 +34,7 @@ private:
|
|||||||
QMap<QString, QThread *> m_threads;
|
QMap<QString, QThread *> m_threads;
|
||||||
QMap<QString, QTimer *> m_timers;
|
QMap<QString, QTimer *> m_timers;
|
||||||
QMap<QString, QByteArray> m_screenshots; // deviceId → скриншот от скрипта
|
QMap<QString, QByteArray> m_screenshots; // deviceId → скриншот от скрипта
|
||||||
QMap<QString, QJsonObject> m_parsedTxs; // key → распарсенная транзакция для FETCH_TRANSACTIONS
|
QMap<QString, int> m_parsedTxCount; // key → сколько транзакций уже отправлено на сервер (FETCH_TRANSACTIONS, per-tx streaming)
|
||||||
QMap<int, int> m_retryCount; // event.id → число уже сделанных ретраев (только FETCH_TRANSACTIONS)
|
QMap<int, int> m_retryCount; // event.id → число уже сделанных ретраев (только FETCH_TRANSACTIONS)
|
||||||
QTimer *m_pollTimer = nullptr;
|
QTimer *m_pollTimer = nullptr;
|
||||||
NetworkService *m_network = nullptr;
|
NetworkService *m_network = nullptr;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user