From 615febc8b545080153ae8b993dce3035e6e0b611 Mon Sep 17 00:00:00 2001 From: slava Date: Wed, 3 Jun 2026 15:29:04 +0500 Subject: [PATCH] Introduce global transaction cache to prevent duplicate confirmations across tasks and enhance workflow consistency. Optimize cache cleanup and integrate validation in "Get --- .../dushanbe/GetLastTransactionsScript.cpp | 14 +++++++++ android/dushanbe/GetLastTransactionsScript.h | 8 +++++ android/ozon/GetLastTransactionsScript.cpp | 14 +++++++++ android/ozon/GetLastTransactionsScript.h | 8 +++++ services/EventHandler.cpp | 30 +++++++++++++++++++ services/EventHandler.h | 2 ++ 6 files changed, 76 insertions(+) diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp index f783936..18a46f0 100644 --- a/android/dushanbe/GetLastTransactionsScript.cpp +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -986,6 +986,20 @@ void GetLastTransactionsScript::searchAll( txJson["real_created_at"] = txTime.isValid() ? txTime.toUTC().toString(Qt::ISODate) : QJsonValue(QJsonValue::Null); + // Проверяем глобальный кэш: если транзакция уже была найдена + // в рамках другой задачи этого материала — пропускаем её. + const QString realAt = txJson["real_created_at"].toString(); + if (!realAt.isEmpty()) { + const QString ck = realAt + "|" + QString::number(qAbs(txJson["amount"].toInt())); + if (m_foundCache.contains(ck)) { + qDebug() << "[Dushanbe::GetLastTransactions] TX already found in another task, skip:" << ck; + AdbUtils::goBack(m_deviceId); + QThread::msleep(500); + continue; + } + m_foundCache.insert(ck); + } + qDebug() << "[Dushanbe::GetLastTransactions] Emitting parsed tx:" << QJsonDocument(txJson).toJson(QJsonDocument::Compact); emit transactionParsed(txJson); diff --git a/android/dushanbe/GetLastTransactionsScript.h b/android/dushanbe/GetLastTransactionsScript.h index f04cc61..3ee56ca 100644 --- a/android/dushanbe/GetLastTransactionsScript.h +++ b/android/dushanbe/GetLastTransactionsScript.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "db/DeviceInfo.h" #include "db/BankProfileInfo.h" @@ -30,6 +31,12 @@ signals: // повторного чтения из БД. void transactionParsed(const QJsonObject &tx); +public: + // Снимок глобального кэша найденных транзакций (от EventHandler). + // Ключи: "real_created_at|abs_amount_cents". Транзакции из этого набора + // пропускаются при поиске, чтобы одна и та же TX не подтверждала два задания. + void setFoundCache(const QSet &cache) { m_foundCache = cache; } + protected: void doStart() override; @@ -57,6 +64,7 @@ private: const QList> m_targets; const QString m_materialId; // UUID задачи (для сохранения дампов) QString m_error; + QSet m_foundCache; // снимок глобального кэша (от EventHandler) }; } // namespace Dushanbe diff --git a/android/ozon/GetLastTransactionsScript.cpp b/android/ozon/GetLastTransactionsScript.cpp index a5c45de..890628d 100644 --- a/android/ozon/GetLastTransactionsScript.cpp +++ b/android/ozon/GetLastTransactionsScript.cpp @@ -636,6 +636,20 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI << "amount=" << parsed.amount << "time=" << dtStr << "name=" << detail.name; + // Проверяем глобальный кэш: если транзакция уже была найдена + // в рамках другой задачи этого материала — пропускаем её. + if (txTime.isValid()) { + const QString ck = txTime.toUTC().toString(Qt::ISODate) + + "|" + QString::number(qAbs(static_cast(parsed.amount * 100))); + if (m_foundCache.contains(ck)) { + qDebug() << "[FetchTransactions] TX already found in another task, skip:" << ck; + AdbUtils::goBack(m_deviceId); + QThread::msleep(500); + continue; // следующая кнопка в for(bi) — ищем другую транзакцию + } + m_foundCache.insert(ck); + } + // Скриншот экрана деталей транзакции — отдаём в EventHandler. // Раньше дополнительно тапали "Документы" + OCR ради ID операции, // но это медленно и нестабильно. Теперь полагаемся на дату+время+ diff --git a/android/ozon/GetLastTransactionsScript.h b/android/ozon/GetLastTransactionsScript.h index 6c3a2ef..6c2cebc 100644 --- a/android/ozon/GetLastTransactionsScript.h +++ b/android/ozon/GetLastTransactionsScript.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "db/DeviceInfo.h" #include "db/BankProfileInfo.h" @@ -16,6 +17,12 @@ signals: // Испускается на КАЖДУЮ найденную транзакцию из списка целей. void transactionParsed(const QJsonObject &tx); +public: + // Снимок глобального кэша найденных транзакций (от EventHandler). + // Ключи: "real_created_at|abs_amount_cents". Транзакции из этого набора + // пропускаются при поиске, чтобы одна и та же TX не подтверждала два задания. + void setFoundCache(const QSet &cache) { m_foundCache = cache; } + public: explicit GetLastTransactionsScript( QString deviceId, @@ -54,6 +61,7 @@ private: const QList> m_targets; const QString m_materialId; // UUID задачи (для сохранения дампов) QString m_error; + QSet m_foundCache; // снимок глобального кэша (от EventHandler) }; } // namespace Ozon diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index 154eb88..f2e78a5 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -38,6 +38,11 @@ static void sendTaskResult(const QString &type, const QString &materialId, const static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description, const QByteArray &screenshot = {}, int eventId = -1, const QString &transactionId = {}, int accountId = -1, const QString &taskJson = {}); static void sendMonitoringLog(const QString &type, const QString &event, const QString &message, const QByteArray &image = {}, int eventId = -1, bool toApiBase = false); +static QString foundTxKey(const QString &materialId, const QJsonObject &tx) { + return materialId + "|" + tx["real_created_at"].toString() + + "|" + QString::number(qAbs(tx["amount"].toInt())); +} + EventHandler::EventHandler(QObject *parent) : QObject(parent) {} EventHandler::~EventHandler() = default; @@ -53,6 +58,13 @@ void EventHandler::start() { // На время установки обновления не трогаем устройства и не запрашиваем задачи. if (UpdatePause::isPaused()) return; + // Удаляем устаревшие записи кэша найденных транзакций (TTL 20 минут) + { + const qint64 now = QDateTime::currentSecsSinceEpoch(); + for (auto it = m_foundTxCache.begin(); it != m_foundTxCache.end(); ) + it = (now - it.value() > 1200) ? m_foundTxCache.erase(it) : ++it; + } + // Пробуем запустить задачи из очереди для свободных устройств const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes(); for (const QString &deviceId : onlineDevices) { @@ -794,11 +806,28 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn arr.append(tx); sendTaskResult(eventTypeToString(EventType::FetchTransactions), event.externalId, event.bankName, arr, event.id, /*updateStatus=*/false); + // Фиксируем в глобальном кэше: будущие задачи того же материала + // не будут повторно подтверждать эту транзакцию (TTL 20 минут). + const QString txKey = foundTxKey(event.externalId, tx); + if (!txKey.startsWith("|")) // real_created_at непустой + m_foundTxCache[txKey] = QDateTime::currentSecsSinceEpoch(); }; + // Снимок кэша найденных транзакций для этого материала (без prefix). + // Скрипт использует его для пропуска уже подтверждённых транзакций. + QSet foundCacheSnapshot; + { + const qint64 now = QDateTime::currentSecsSinceEpoch(); + const QString prefix = event.externalId + "|"; + for (auto it = m_foundTxCache.cbegin(); it != m_foundTxCache.cend(); ++it) + if (now - it.value() < 1200 && it.key().startsWith(prefix)) + foundCacheSnapshot.insert(it.key().mid(prefix.size())); + } + if (appCode == "ozon") { auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, targets, account.materialId); setup(w); connectScreenshot(w); + w->setFoundCache(foundCacheSnapshot); connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this, streamParsedTx); } else if (appCode == "black") @@ -806,6 +835,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn else if (appCode == "dushanbe_city_bank") { auto *w = new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, targets, account.materialId); setup(w); connectScreenshotDushanbe(w); + w->setFoundCache(foundCacheSnapshot); connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this, streamParsedTx); } else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode; diff --git a/services/EventHandler.h b/services/EventHandler.h index 403dc68..db34c44 100644 --- a/services/EventHandler.h +++ b/services/EventHandler.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "db/MaterialInfo.h" @@ -36,6 +37,7 @@ private: QMap m_screenshots; // deviceId → скриншот от скрипта QMap m_parsedTxCount; // key → сколько транзакций уже отправлено на сервер (FETCH_TRANSACTIONS, per-tx streaming) QMap m_parsedTxJson; // key → сами отправленные tx (для показа JSON в task_success мониторинга) + QMap m_foundTxCache; // "materialId|real_created_at|abs_amount_cents" → unix timestamp; защита от двойного подтверждения одной TX двумя задачами QMap m_retryCount; // event.id → число уже сделанных ретраев (только FETCH_TRANSACTIONS) QTimer *m_pollTimer = nullptr; NetworkService *m_network = nullptr;