1
0
forked from BRT/arc

Introduce global transaction cache to prevent duplicate confirmations across tasks and enhance workflow consistency. Optimize cache cleanup and integrate validation in "Get

This commit is contained in:
slava 2026-06-03 15:29:04 +05:00
parent cd98644f32
commit 615febc8b5
6 changed files with 76 additions and 0 deletions

View File

@ -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);

View File

@ -5,6 +5,7 @@
#include <QList>
#include <QPair>
#include <QPoint>
#include <QSet>
#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<QString> &cache) { m_foundCache = cache; }
protected:
void doStart() override;
@ -57,6 +64,7 @@ private:
const QList<QPair<double, QDateTime>> m_targets;
const QString m_materialId; // UUID задачи (для сохранения дампов)
QString m_error;
QSet<QString> m_foundCache; // снимок глобального кэша (от EventHandler)
};
} // namespace Dushanbe

View File

@ -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<int>(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 операции,
// но это медленно и нестабильно. Теперь полагаемся на дату+время+

View File

@ -4,6 +4,7 @@
#include <QJsonObject>
#include <QList>
#include <QPair>
#include <QSet>
#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<QString> &cache) { m_foundCache = cache; }
public:
explicit GetLastTransactionsScript(
QString deviceId,
@ -54,6 +61,7 @@ private:
const QList<QPair<double, QDateTime>> m_targets;
const QString m_materialId; // UUID задачи (для сохранения дампов)
QString m_error;
QSet<QString> m_foundCache; // снимок глобального кэша (от EventHandler)
};
} // namespace Ozon

View File

@ -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<QString> 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;

View File

@ -3,6 +3,7 @@
#include <QJsonObject>
#include <QMap>
#include <QObject>
#include <QSet>
#include <QTimer>
#include "db/MaterialInfo.h"
@ -36,6 +37,7 @@ private:
QMap<QString, QByteArray> m_screenshots; // deviceId → скриншот от скрипта
QMap<QString, int> m_parsedTxCount; // key → сколько транзакций уже отправлено на сервер (FETCH_TRANSACTIONS, per-tx streaming)
QMap<QString, QJsonArray> m_parsedTxJson; // key → сами отправленные tx (для показа JSON в task_success мониторинга)
QMap<QString, qint64> m_foundTxCache; // "materialId|real_created_at|abs_amount_cents" → unix timestamp; защита от двойного подтверждения одной TX двумя задачами
QMap<int, int> m_retryCount; // event.id → число уже сделанных ретраев (только FETCH_TRANSACTIONS)
QTimer *m_pollTimer = nullptr;
NetworkService *m_network = nullptr;