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:
parent
cd98644f32
commit
615febc8b5
@ -986,6 +986,20 @@ void GetLastTransactionsScript::searchAll(
|
|||||||
txJson["real_created_at"] = txTime.isValid()
|
txJson["real_created_at"] = txTime.isValid()
|
||||||
? txTime.toUTC().toString(Qt::ISODate) : QJsonValue(QJsonValue::Null);
|
? 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:"
|
qDebug() << "[Dushanbe::GetLastTransactions] Emitting parsed tx:"
|
||||||
<< QJsonDocument(txJson).toJson(QJsonDocument::Compact);
|
<< QJsonDocument(txJson).toJson(QJsonDocument::Compact);
|
||||||
emit transactionParsed(txJson);
|
emit transactionParsed(txJson);
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QPair>
|
#include <QPair>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
|
#include <QSet>
|
||||||
#include "db/DeviceInfo.h"
|
#include "db/DeviceInfo.h"
|
||||||
#include "db/BankProfileInfo.h"
|
#include "db/BankProfileInfo.h"
|
||||||
|
|
||||||
@ -30,6 +31,12 @@ signals:
|
|||||||
// повторного чтения из БД.
|
// повторного чтения из БД.
|
||||||
void transactionParsed(const QJsonObject &tx);
|
void transactionParsed(const QJsonObject &tx);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Снимок глобального кэша найденных транзакций (от EventHandler).
|
||||||
|
// Ключи: "real_created_at|abs_amount_cents". Транзакции из этого набора
|
||||||
|
// пропускаются при поиске, чтобы одна и та же TX не подтверждала два задания.
|
||||||
|
void setFoundCache(const QSet<QString> &cache) { m_foundCache = cache; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void doStart() override;
|
void doStart() override;
|
||||||
|
|
||||||
@ -57,6 +64,7 @@ private:
|
|||||||
const QList<QPair<double, QDateTime>> m_targets;
|
const QList<QPair<double, QDateTime>> m_targets;
|
||||||
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
||||||
QString m_error;
|
QString m_error;
|
||||||
|
QSet<QString> m_foundCache; // снимок глобального кэша (от EventHandler)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Dushanbe
|
} // namespace Dushanbe
|
||||||
|
|||||||
@ -636,6 +636,20 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
|
|||||||
<< "amount=" << parsed.amount << "time=" << dtStr
|
<< "amount=" << parsed.amount << "time=" << dtStr
|
||||||
<< "name=" << detail.name;
|
<< "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.
|
// Скриншот экрана деталей транзакции — отдаём в EventHandler.
|
||||||
// Раньше дополнительно тапали "Документы" + OCR ради ID операции,
|
// Раньше дополнительно тапали "Документы" + OCR ради ID операции,
|
||||||
// но это медленно и нестабильно. Теперь полагаемся на дату+время+
|
// но это медленно и нестабильно. Теперь полагаемся на дату+время+
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QPair>
|
#include <QPair>
|
||||||
|
#include <QSet>
|
||||||
#include "db/DeviceInfo.h"
|
#include "db/DeviceInfo.h"
|
||||||
#include "db/BankProfileInfo.h"
|
#include "db/BankProfileInfo.h"
|
||||||
|
|
||||||
@ -16,6 +17,12 @@ signals:
|
|||||||
// Испускается на КАЖДУЮ найденную транзакцию из списка целей.
|
// Испускается на КАЖДУЮ найденную транзакцию из списка целей.
|
||||||
void transactionParsed(const QJsonObject &tx);
|
void transactionParsed(const QJsonObject &tx);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Снимок глобального кэша найденных транзакций (от EventHandler).
|
||||||
|
// Ключи: "real_created_at|abs_amount_cents". Транзакции из этого набора
|
||||||
|
// пропускаются при поиске, чтобы одна и та же TX не подтверждала два задания.
|
||||||
|
void setFoundCache(const QSet<QString> &cache) { m_foundCache = cache; }
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit GetLastTransactionsScript(
|
explicit GetLastTransactionsScript(
|
||||||
QString deviceId,
|
QString deviceId,
|
||||||
@ -54,6 +61,7 @@ private:
|
|||||||
const QList<QPair<double, QDateTime>> m_targets;
|
const QList<QPair<double, QDateTime>> m_targets;
|
||||||
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
||||||
QString m_error;
|
QString m_error;
|
||||||
|
QSet<QString> m_foundCache; // снимок глобального кэша (от EventHandler)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Ozon
|
} // namespace Ozon
|
||||||
|
|||||||
@ -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 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 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(QObject *parent) : QObject(parent) {}
|
||||||
|
|
||||||
EventHandler::~EventHandler() = default;
|
EventHandler::~EventHandler() = default;
|
||||||
@ -53,6 +58,13 @@ void EventHandler::start() {
|
|||||||
// На время установки обновления не трогаем устройства и не запрашиваем задачи.
|
// На время установки обновления не трогаем устройства и не запрашиваем задачи.
|
||||||
if (UpdatePause::isPaused()) return;
|
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();
|
const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes();
|
||||||
for (const QString &deviceId : onlineDevices) {
|
for (const QString &deviceId : onlineDevices) {
|
||||||
@ -794,11 +806,28 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
|||||||
arr.append(tx);
|
arr.append(tx);
|
||||||
sendTaskResult(eventTypeToString(EventType::FetchTransactions),
|
sendTaskResult(eventTypeToString(EventType::FetchTransactions),
|
||||||
event.externalId, event.bankName, arr, event.id, /*updateStatus=*/false);
|
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") {
|
if (appCode == "ozon") {
|
||||||
auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, targets, account.materialId);
|
auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, targets, account.materialId);
|
||||||
setup(w); connectScreenshot(w);
|
setup(w); connectScreenshot(w);
|
||||||
|
w->setFoundCache(foundCacheSnapshot);
|
||||||
connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this, streamParsedTx);
|
connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this, streamParsedTx);
|
||||||
}
|
}
|
||||||
else if (appCode == "black")
|
else if (appCode == "black")
|
||||||
@ -806,6 +835,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
|||||||
else if (appCode == "dushanbe_city_bank") {
|
else if (appCode == "dushanbe_city_bank") {
|
||||||
auto *w = new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, targets, account.materialId);
|
auto *w = new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, targets, account.materialId);
|
||||||
setup(w); connectScreenshotDushanbe(w);
|
setup(w); connectScreenshotDushanbe(w);
|
||||||
|
w->setFoundCache(foundCacheSnapshot);
|
||||||
connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this, streamParsedTx);
|
connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this, streamParsedTx);
|
||||||
}
|
}
|
||||||
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QSet>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "db/MaterialInfo.h"
|
#include "db/MaterialInfo.h"
|
||||||
@ -36,6 +37,7 @@ private:
|
|||||||
QMap<QString, QByteArray> m_screenshots; // deviceId → скриншот от скрипта
|
QMap<QString, QByteArray> m_screenshots; // deviceId → скриншот от скрипта
|
||||||
QMap<QString, int> m_parsedTxCount; // key → сколько транзакций уже отправлено на сервер (FETCH_TRANSACTIONS, per-tx streaming)
|
QMap<QString, int> m_parsedTxCount; // key → сколько транзакций уже отправлено на сервер (FETCH_TRANSACTIONS, per-tx streaming)
|
||||||
QMap<QString, QJsonArray> m_parsedTxJson; // key → сами отправленные tx (для показа JSON в task_success мониторинга)
|
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)
|
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