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(
|
||||
QString deviceId,
|
||||
QString appCode,
|
||||
double searchAmount,
|
||||
QDateTime searchTime,
|
||||
QList<QPair<double, QDateTime>> targets,
|
||||
QString materialId,
|
||||
QObject *parent
|
||||
) : CommonScript(parent),
|
||||
m_deviceId(std::move(deviceId)),
|
||||
m_appCode(std::move(appCode)),
|
||||
m_searchAmount(searchAmount),
|
||||
m_searchTime(std::move(searchTime)),
|
||||
m_targets(std::move(targets)),
|
||||
m_materialId(std::move(materialId)) {
|
||||
}
|
||||
|
||||
@ -277,7 +275,8 @@ void GetLastTransactionsScript::doStart() {
|
||||
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);
|
||||
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
@ -378,9 +377,6 @@ void GetLastTransactionsScript::doStart() {
|
||||
}
|
||||
}
|
||||
|
||||
// Скриншот списка транзакций — отправим при ошибке, удалим при успехе
|
||||
const QByteArray listScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId);
|
||||
|
||||
// Находим accountId
|
||||
int accountId = -1;
|
||||
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(device.id, m_appCode);
|
||||
@ -390,8 +386,62 @@ void GetLastTransactionsScript::doStart() {
|
||||
qWarning() << "[Dushanbe::GetLastTransactions] No account found";
|
||||
}
|
||||
|
||||
qDebug() << "[Dushanbe::GetLastTransactions] Searching: amount=" << m_searchAmount
|
||||
<< "time=" << m_searchTime.toString(Qt::ISODate);
|
||||
// Ищем каждую цель из списка. Найденные отдаём сразу через transactionParsed;
|
||||
// ненайденные копим и репортим одной агрегированной ошибкой в конце.
|
||||
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), не устройства
|
||||
const QTimeZone tjTz("Asia/Dushanbe");
|
||||
@ -412,8 +462,7 @@ void GetLastTransactionsScript::doStart() {
|
||||
for (int scroll = 0; scroll <= maxScrolls; ++scroll) {
|
||||
if (interrupted()) {
|
||||
m_error = "Interrupted";
|
||||
emit finishedWithResult(m_error);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||
@ -602,7 +651,7 @@ void GetLastTransactionsScript::doStart() {
|
||||
<< "y=" << items[i].node.y();
|
||||
}
|
||||
|
||||
const double searchAbs = qAbs(m_searchAmount);
|
||||
const double searchAbs = qAbs(searchAmount);
|
||||
for (const auto &tx : items) {
|
||||
const double absListAmt = qAbs(tx.amount);
|
||||
const bool amountDirect = qAbs(absListAmt - searchAbs) < 0.01;
|
||||
@ -617,9 +666,9 @@ void GetLastTransactionsScript::doStart() {
|
||||
// Pre-filter по ДАТЕ: только если дата надёжная (из content-desc P2P формата).
|
||||
// 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 searchLocalDate = m_searchTime.toTimeZone(tjTz).date();
|
||||
const QDate searchLocalDate = searchTime.toTimeZone(tjTz).date();
|
||||
if (listDate.isValid() && searchLocalDate.isValid()) {
|
||||
const qint64 dayDiff = qAbs(listDate.daysTo(searchLocalDate));
|
||||
if (dayDiff > 3) {
|
||||
@ -632,9 +681,9 @@ void GetLastTransactionsScript::doStart() {
|
||||
}
|
||||
|
||||
// Проверка времени по 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 searchLocalTime = m_searchTime.toTimeZone(tjTz).time();
|
||||
const QTime searchLocalTime = searchTime.toTimeZone(tjTz).time();
|
||||
if (listTime.isValid() && searchLocalTime.isValid()) {
|
||||
const int diffSecs = qAbs(listTime.secsTo(searchLocalTime));
|
||||
if (diffSecs > 600 && diffSecs < 85800) { // 85800 = 23h50m
|
||||
@ -665,13 +714,13 @@ void GetLastTransactionsScript::doStart() {
|
||||
const int tapX = txNode.x1() + 80;
|
||||
AdbUtils::makeTap(m_deviceId, tapX, txNode.y());
|
||||
QThread::msleep(3000);
|
||||
if (interrupted()) { m_error = "Interrupted"; emit finishedWithResult(m_error); return; }
|
||||
if (interrupted()) { m_error = "Interrupted"; return false; }
|
||||
|
||||
// Ждём экран деталей Выписки — ImageView с content-desc начинающимся с "Успешный платеж"
|
||||
OcrTransactionDetail detail;
|
||||
bool detailLoaded = false;
|
||||
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));
|
||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||
// 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;
|
||||
|
||||
// Проверяем время ±10 минут
|
||||
if (m_searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) {
|
||||
if (searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) {
|
||||
QDateTime txTime = QDateTime::fromString(
|
||||
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
|
||||
if (!txTime.isValid()) {
|
||||
@ -808,7 +857,7 @@ void GetLastTransactionsScript::doStart() {
|
||||
}
|
||||
if (txTime.isValid()) {
|
||||
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) {
|
||||
qDebug() << "[Dushanbe::GetLastTransactions] Time mismatch: diff=" << diffSecs << "sec (>10min), skipping";
|
||||
AdbUtils::goBack(m_deviceId);
|
||||
@ -877,13 +926,12 @@ void GetLastTransactionsScript::doStart() {
|
||||
}
|
||||
QThread::msleep(1000);
|
||||
|
||||
emit finishedWithResult(m_error); // m_error пустой — успех
|
||||
return;
|
||||
return true; // успех — транзакция найдена и отдана через transactionParsed
|
||||
}
|
||||
|
||||
// Скроллим вниз — свайп внутри области транзакций
|
||||
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;
|
||||
// Определяем область контента из реальных Y позиций items
|
||||
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)
|
||||
+ " time=" + m_searchTime.toString(Qt::ISODate);
|
||||
qWarning() << "[Dushanbe::GetLastTransactions]" << m_error;
|
||||
// Эту цель не нашли. m_error не трогаем — агрегированную ошибку по всем
|
||||
// ненайденным целям соберёт оркестратор в doStart (он же вернёт на главную).
|
||||
qWarning() << "[Dushanbe::GetLastTransactions] Transaction not found: amount="
|
||||
<< QString::number(searchAmount, 'f', 2)
|
||||
<< "time=" << searchTime.toString(Qt::ISODate);
|
||||
|
||||
// Отправляем скриншот списка транзакций, сделанный до начала поиска
|
||||
m_resultScreenshot = listScreenshot.isEmpty()
|
||||
? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot;
|
||||
if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot);
|
||||
|
||||
// Возвращаемся на главную
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Dushanbe
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
#include "CommonScript.h"
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include "db/DeviceInfo.h"
|
||||
#include "db/BankProfileInfo.h"
|
||||
|
||||
@ -14,8 +16,7 @@ public:
|
||||
explicit GetLastTransactionsScript(
|
||||
QString deviceId,
|
||||
QString appCode,
|
||||
double searchAmount = 0,
|
||||
QDateTime searchTime = {},
|
||||
QList<QPair<double, QDateTime>> targets = {},
|
||||
QString materialId = {},
|
||||
QObject *parent = nullptr
|
||||
);
|
||||
@ -23,7 +24,7 @@ public:
|
||||
~GetLastTransactionsScript() override;
|
||||
|
||||
signals:
|
||||
// Испускается, когда транзакция найдена и распарсена с экрана деталей.
|
||||
// Испускается на КАЖДУЮ найденную транзакцию из списка целей.
|
||||
// EventHandler использует эти данные напрямую для task_result вместо
|
||||
// повторного чтения из БД.
|
||||
void transactionParsed(const QJsonObject &tx);
|
||||
@ -32,10 +33,19 @@ protected:
|
||||
void doStart() override;
|
||||
|
||||
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;
|
||||
const QString m_appCode;
|
||||
const double m_searchAmount; // сумма в сомони (0 = все последние)
|
||||
const QDateTime m_searchTime; // примерное время (UTC)
|
||||
// Список целей: пары (сумма в сомони со знаком, примерное время UTC).
|
||||
const QList<QPair<double, QDateTime>> m_targets;
|
||||
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
@ -179,6 +179,14 @@ bool CommonScript::runAppAndGoToHomeScreen(
|
||||
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) {
|
||||
const QList<Node> pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||||
@ -225,6 +233,12 @@ bool CommonScript::tryToCloseAllBannersOnHomePage(const QString &deviceId, const
|
||||
closedAny = true;
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||
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);
|
||||
closeBannerOrDialogNode.x() != -1 && closeBannerOrDialogNode.y() != -1) {
|
||||
AdbUtils::makeTap(deviceId, closeBannerOrDialogNode.x(), closeBannerOrDialogNode.y());
|
||||
|
||||
@ -82,16 +82,14 @@ GetLastTransactionsScript::GetLastTransactionsScript(
|
||||
QString deviceId,
|
||||
QString appCode,
|
||||
int maxCount,
|
||||
double searchAmount,
|
||||
QDateTime searchTime,
|
||||
QList<QPair<double, QDateTime>> targets,
|
||||
QString materialId,
|
||||
QObject *parent
|
||||
) : CommonScript(parent),
|
||||
m_deviceId(std::move(deviceId)),
|
||||
m_appCode(std::move(appCode)),
|
||||
m_maxCount(maxCount),
|
||||
m_searchAmount(searchAmount),
|
||||
m_searchTime(std::move(searchTime)),
|
||||
m_targets(std::move(targets)),
|
||||
m_materialId(std::move(materialId)) {
|
||||
}
|
||||
|
||||
@ -110,7 +108,8 @@ void GetLastTransactionsScript::doStart() {
|
||||
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);
|
||||
|
||||
if (!navigateToTransactionList(device, app)) {
|
||||
@ -127,9 +126,31 @@ void GetLastTransactionsScript::doStart() {
|
||||
qWarning() << "[GetLastTransactions] No account found for" << device.id << m_appCode;
|
||||
}
|
||||
|
||||
// Выбор режима: поиск конкретной транзакции или сканирование последних
|
||||
if (qAbs(m_searchAmount) > 0.01) {
|
||||
searchTransaction(device, accountId);
|
||||
// Выбор режима: поиск списка конкретных транзакций или сканирование последних.
|
||||
if (!m_targets.isEmpty()) {
|
||||
// Ищем каждую цель из списка. Найденные отдаём сразу через
|
||||
// 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 {
|
||||
scanRecentTransactions(device, accountId);
|
||||
}
|
||||
@ -242,8 +263,9 @@ bool GetLastTransactionsScript::navigateToTransactionList(const DeviceInfo &devi
|
||||
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);
|
||||
|
||||
// Время в Ozon показывается в таймзоне устройства, а не в MSK
|
||||
@ -266,12 +288,12 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
int navBarTop = detectNavBarTop();
|
||||
|
||||
// Целевая дата в таймзоне устройства для поиска заголовка дня в списке
|
||||
const QDateTime searchLocal = m_searchTime.toTimeZone(deviceTz);
|
||||
const QDateTime searchLocal = searchTime.toTimeZone(deviceTz);
|
||||
const QDate searchDate = searchLocal.date();
|
||||
|
||||
qDebug() << "[FetchTransactions] Searching: device=" << m_deviceId
|
||||
<< "amount=" << m_searchAmount
|
||||
<< "time=" << m_searchTime.toString(Qt::ISODate)
|
||||
<< "amount=" << searchAmount
|
||||
<< "time=" << searchTime.toString(Qt::ISODate)
|
||||
<< "tz=" << tzName
|
||||
<< "date(local)=" << searchDate.toString("dd.MM.yyyy")
|
||||
<< "screen=" << device.screenWidth << "x" << device.screenHeight;
|
||||
@ -348,7 +370,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
for (int scroll = 0; scroll < maxScrolls; ++scroll) {
|
||||
if (QThread::currentThread()->isInterruptionRequested()) {
|
||||
m_error = "Interrupted";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||
@ -369,12 +391,12 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
}
|
||||
|
||||
if (!dateFound) {
|
||||
m_error = "Date not found in transaction list: " + searchDate.toString("dd.MM.yyyy");
|
||||
qWarning() << "[FetchTransactions]" << m_error;
|
||||
qWarning() << "[FetchTransactions] Date not found in transaction list: "
|
||||
<< searchDate.toString("dd.MM.yyyy");
|
||||
m_resultScreenshot = listScreenshot.isEmpty()
|
||||
? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot;
|
||||
if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Хелпер: находит Y-диапазон [dateY, nextDateY) для нужной даты на экране.
|
||||
@ -436,7 +458,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
for (int attempt = 0; attempt < 15; ++attempt) {
|
||||
if (QThread::currentThread()->isInterruptionRequested()) {
|
||||
m_error = "Interrupted";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
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) {
|
||||
if (QThread::currentThread()->isInterruptionRequested()) {
|
||||
m_error = "Interrupted";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Пропускаем кнопки вне Y-диапазона нужной даты.
|
||||
@ -517,9 +539,9 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
|
||||
const TransactionInfo parsed = xmlScreenParser.parseTransactionButtonText(txButtons[bi].text);
|
||||
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);
|
||||
if (qAbs(parsed.amount - m_searchAmount) > 0.01) continue;
|
||||
if (qAbs(parsed.amount - searchAmount) > 0.01) continue;
|
||||
|
||||
const Node &visibleBtn = txButtons[bi];
|
||||
// Зажимаем тап-Y на видимую часть кнопки: если кнопка заходит под
|
||||
@ -543,7 +565,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
for (int da = 0; da < 10; ++da) {
|
||||
if (QThread::currentThread()->isInterruptionRequested()) {
|
||||
m_error = "Interrupted";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||
@ -597,8 +619,8 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
}
|
||||
|
||||
// Проверяем время (±10 минут)
|
||||
if (txTime.isValid() && m_searchTime.isValid()) {
|
||||
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC()));
|
||||
if (txTime.isValid() && searchTime.isValid()) {
|
||||
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(searchTime.toUTC()));
|
||||
if (diffSecs > 600) {
|
||||
qDebug() << "[FetchTransactions] Time mismatch:" << dtStr
|
||||
<< "expected" << searchLocal.toString("dd.MM.yyyy, HH:mm")
|
||||
@ -680,7 +702,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
QThread::msleep(1000);
|
||||
AdbUtils::goBack(m_deviceId);
|
||||
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
|
||||
+ " amount=" + QString::number(m_searchAmount, 'f', 2)
|
||||
+ " date=" + searchDate.toString("dd.MM.yyyy");
|
||||
qWarning() << "[FetchTransactions]" << m_error;
|
||||
// Эту цель не нашли. m_error не трогаем — агрегированную ошибку по всем
|
||||
// ненайденным целям соберёт оркестратор в doStart.
|
||||
qWarning() << "[FetchTransactions] Transaction not found: device=" << m_deviceId
|
||||
<< "amount=" << QString::number(searchAmount, 'f', 2)
|
||||
<< "date=" << searchDate.toString("dd.MM.yyyy");
|
||||
// Отправляем скриншот списка транзакций, сделанный до начала поиска
|
||||
m_resultScreenshot = listScreenshot.isEmpty()
|
||||
? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot;
|
||||
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) {
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
#include "CommonScript.h"
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include "db/DeviceInfo.h"
|
||||
#include "db/BankProfileInfo.h"
|
||||
|
||||
@ -11,6 +13,7 @@ class GetLastTransactionsScript final : public CommonScript {
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
// Испускается на КАЖДУЮ найденную транзакцию из списка целей.
|
||||
void transactionParsed(const QJsonObject &tx);
|
||||
|
||||
public:
|
||||
@ -18,8 +21,7 @@ public:
|
||||
QString deviceId,
|
||||
QString appCode,
|
||||
int maxCount = 0,
|
||||
double searchAmount = 0,
|
||||
QDateTime searchTime = {},
|
||||
QList<QPair<double, QDateTime>> targets = {},
|
||||
QString materialId = {},
|
||||
QObject *parent = nullptr
|
||||
);
|
||||
@ -36,14 +38,20 @@ private:
|
||||
// Сканирование последних транзакций (для UI: 24ч или maxCount)
|
||||
void scanRecentTransactions(const DeviceInfo &device, int accountId);
|
||||
|
||||
// Поиск конкретной транзакции по сумме и времени (для задач FETCH_TRANSACTIONS)
|
||||
void searchTransaction(const DeviceInfo &device, int accountId);
|
||||
// Поиск одной конкретной транзакции по сумме и времени (FETCH_TRANSACTIONS).
|
||||
// Возвращает true и эмитит transactionParsed, если нашли; false — если нет.
|
||||
bool searchOne(const DeviceInfo &device, int accountId,
|
||||
double searchAmount, const QDateTime &searchTime);
|
||||
|
||||
// Быстрый возврат к началу списка операций (между поисками целей)
|
||||
void scrollListToTop(const DeviceInfo &device);
|
||||
|
||||
QString m_deviceId;
|
||||
const QString m_appCode;
|
||||
const int m_maxCount; // 0 = фильтр по 24ч, >0 = N последних
|
||||
const double m_searchAmount; // сумма в рублях (0 = не искать конкретную)
|
||||
const QDateTime m_searchTime; // примерное время (UTC)
|
||||
// Список целей поиска: пары (сумма в рублях со знаком, примерное время UTC).
|
||||
// Пустой список → режим сканирования последних (scanRecentTransactions).
|
||||
const QList<QPair<double, QDateTime>> m_targets;
|
||||
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
@ -323,6 +323,28 @@ Node ScreenXmlParser::tryToFindGooglePlayBanner(const int screenWidth, const int
|
||||
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() {
|
||||
// Страница профиля Ozon: есть заголовок "Аккаунт" и кнопка "Выйти"
|
||||
bool hasTitle = false;
|
||||
|
||||
@ -50,6 +50,11 @@ public:
|
||||
|
||||
Node tryToFindGooglePlayBanner(int screenWidth, int screenHeight);
|
||||
|
||||
// Онбординг-баннер "Обновили главную" (resource-id onboarding_viewpager),
|
||||
// появляется поверх главного экрана Ozon. Возвращает кнопку "Пропустить"
|
||||
// для тапа, либо пустой Node если баннер не виден.
|
||||
Node tryToFindOnboardingBanner();
|
||||
|
||||
// Находит кнопку "ЗАПРЕТИТЬ" в системном диалоге запроса разрешения Android
|
||||
// (com.google.android.permissioncontroller). Возвращает пустой Node если диалог не виден.
|
||||
Node tryToFindSystemPermissionDenyButton();
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
#include "dushanbe/PayByCardScript.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 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,
|
||||
const QJsonValue &data, int eventId) {
|
||||
const QJsonValue &data, int eventId, bool updateStatus) {
|
||||
QJsonObject obj;
|
||||
obj["type"] = type;
|
||||
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());
|
||||
QObject::connect(thread, &QThread::started, networkService, &NetworkService::postTaskResult);
|
||||
QObject::connect(networkService, &NetworkService::finishedWithResult, networkService,
|
||||
[eventId](int result) {
|
||||
if (eventId != -1) {
|
||||
[eventId, updateStatus](int result) {
|
||||
// updateStatus=false для потоковых per-tx результатов FETCH_TRANSACTIONS:
|
||||
// статус события финализируется один раз в updateEventThread, а не на
|
||||
// каждой отдельной отправке найденной транзакции.
|
||||
if (eventId != -1 && updateStatus) {
|
||||
if (result == 0) {
|
||||
EventDAO::markSentToServer(eventId);
|
||||
} else {
|
||||
@ -288,9 +291,11 @@ bool EventHandler::tryRetry(const QString &key, const EventInfo &event, const QS
|
||||
.arg(QString::number(attempt), QString::number(kMaxRetries),
|
||||
event.deviceId, event.externalId, reason));
|
||||
|
||||
// Чистим артефакты прошлой попытки
|
||||
// Чистим артефакты прошлой попытки. Ретрай запускается только когда ещё
|
||||
// ничего не отправлено (см. guard в updateEventThread), так что сбрасываем
|
||||
// счётчик отправленных в 0.
|
||||
m_screenshots.remove(key);
|
||||
m_parsedTxs.remove(key);
|
||||
m_parsedTxCount.remove(key);
|
||||
|
||||
// Снимаем текущий поток/таймер (в timeout-ветке они уже сняты — take() вернёт null)
|
||||
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) {
|
||||
|
||||
// Поиск транзакции: при ошибке скрипта перезапускаем (ещё до 2 раз) вместо
|
||||
// немедленной отправки ошибки на сервер.
|
||||
// немедленной отправки ошибки на сервер. Но только если НИ ОДНОЙ транзакции
|
||||
// ещё не отправлено: при частичном успехе (часть списка найдена и уже
|
||||
// улетела на сервер) ретрай бессмыслен — переотправит дубли, поэтому
|
||||
// ненайденные просто репортим ошибкой ниже.
|
||||
if (!result.isEmpty() && event.type == EventType::FetchTransactions
|
||||
&& m_parsedTxCount.value(key, 0) == 0
|
||||
&& tryRetry(key, event, result)) {
|
||||
return;
|
||||
}
|
||||
@ -413,24 +422,13 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
||||
obj["currency"] = currencyCode;
|
||||
taskData = obj;
|
||||
} else if (event.type == EventType::FetchTransactions) {
|
||||
// FETCH_TRANSACTIONS больше НЕ читает из БД — берём то, что
|
||||
// скрипт распарсил вживую и передал через сигнал transactionParsed.
|
||||
const QJsonObject parsedTx = m_parsedTxs.take(key);
|
||||
if (parsedTx.isEmpty()) {
|
||||
qWarning() << "[EventHandler] FETCH_TRANSACTIONS: script did not emit parsed tx";
|
||||
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");
|
||||
// FETCH_TRANSACTIONS шлёт результаты потоково: каждая найденная
|
||||
// транзакция уже отправлена отдельным task_result по сигналу
|
||||
// transactionParsed (см. startThreadForTask). Здесь, при полном
|
||||
// успехе (m_error пуст → найдены все цели списка), отдельный
|
||||
// финальный task_result НЕ отправляем — только финализируем
|
||||
// событие, чтобы оно не считалось «зависшим» при перезапуске.
|
||||
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) {
|
||||
// Определяем тип транзакции из JSON задачи
|
||||
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";
|
||||
taskData = obj;
|
||||
}
|
||||
// FETCH_TRANSACTIONS уже отправил данные потоково (per-tx) —
|
||||
// финальный агрегирующий результат не нужен.
|
||||
if (event.type != EventType::FetchTransactions) {
|
||||
sendTaskResult(
|
||||
eventTypeToString(event.type),
|
||||
event.externalId,
|
||||
@ -475,6 +476,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
||||
event.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (newStatus == EventStatus::Complete) {
|
||||
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_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) {
|
||||
// Парсим amount и created_at из body задачи
|
||||
// body теперь — массив транзакций для поиска: [{amount, created_at}, ...].
|
||||
// Для обратной совместимости поддерживаем и старый формат — одиночный
|
||||
// объект {amount, created_at} (оборачиваем в массив из одного элемента).
|
||||
const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object();
|
||||
const QJsonObject body = taskJson["body"].toObject();
|
||||
const double searchAmount = body["amount"].toDouble() / 100.0; // копейки → рубли (с учётом знака)
|
||||
const QString createdAtStr = body["created_at"].toString();
|
||||
QDateTime searchTime = QDateTime::fromString(createdAtStr, Qt::ISODate);
|
||||
// Backend контракт — UTC ISO с суффиксом 'Z' (как мы сами пишем
|
||||
// на строках 317 и 373). Если 'Z'/смещение отсутствует — Qt парсит
|
||||
// как LocalTime, что даёт off-by-one для вечерних транзакций
|
||||
// (см. task_dump (26): Ozon группировал +100 на 14.05, backend
|
||||
// прислал 13.05 без TZ). Жёстко переинтерпретируем как UTC.
|
||||
QJsonArray bodyArr = taskJson["body"].toArray();
|
||||
if (bodyArr.isEmpty() && taskJson["body"].isObject())
|
||||
bodyArr.append(taskJson["body"]);
|
||||
|
||||
QList<QPair<double, QDateTime>> targets;
|
||||
for (const auto &v : bodyArr) {
|
||||
const QJsonObject item = v.toObject();
|
||||
const double searchAmount = item["amount"].toDouble() / 100.0; // копейки → рубли (с учётом знака)
|
||||
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) {
|
||||
qWarning() << "[EventHandler] FETCH_TRANSACTIONS: created_at без TZ-маркера"
|
||||
<< createdAtStr << "— интерпретирую как UTC";
|
||||
<< item["created_at"].toString() << "— интерпретирую как 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") {
|
||||
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);
|
||||
connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this,
|
||||
[this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; });
|
||||
connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this, streamParsedTx);
|
||||
}
|
||||
else if (appCode == "black")
|
||||
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
|
||||
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);
|
||||
connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this,
|
||||
[this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; });
|
||||
connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this, streamParsedTx);
|
||||
}
|
||||
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
||||
}
|
||||
@ -712,8 +733,10 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
||||
}
|
||||
m_threads.remove(key);
|
||||
|
||||
// Поиск транзакции: таймаут тоже ретраим (ещё до 2 раз)
|
||||
// Поиск транзакции: таймаут тоже ретраим (ещё до 2 раз), но только если
|
||||
// ещё ни одной транзакции не отправлено (иначе ретрай переотправит дубли).
|
||||
if (event.type == EventType::FetchTransactions
|
||||
&& m_parsedTxCount.value(key, 0) == 0
|
||||
&& tryRetry(key, event, "Script timeout (5 min)")) {
|
||||
return;
|
||||
}
|
||||
@ -812,7 +835,7 @@ void EventHandler::clearAll() {
|
||||
}
|
||||
m_timers.clear();
|
||||
m_screenshots.clear();
|
||||
m_parsedTxs.clear();
|
||||
m_parsedTxCount.clear();
|
||||
}
|
||||
|
||||
void EventHandler::stop() {
|
||||
|
||||
@ -34,7 +34,7 @@ private:
|
||||
QMap<QString, QThread *> m_threads;
|
||||
QMap<QString, QTimer *> m_timers;
|
||||
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)
|
||||
QTimer *m_pollTimer = nullptr;
|
||||
NetworkService *m_network = nullptr;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user