1
0
forked from BRT/arc

Add taskJson parameter to sendTaskError, introduce sendTaskSuccess, enhance error and success logging with detailed JSON data, and refine time mismatch handling.

This commit is contained in:
slava 2026-06-03 15:17:22 +05:00
parent 66279d4767
commit cd98644f32
5 changed files with 142 additions and 35 deletions

View File

@ -740,13 +740,15 @@ void GetLastTransactionsScript::searchAll(
} }
} }
// Проверка времени по HH:MM:SS (±10 минут, с учётом wraparound полуночи) // Проверка времени по HH:MM:SS (+10 минут, одностороннее окно, с учётом wraparound полуночи)
if (cand.isValid() && !tx.time.isEmpty()) { if (cand.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 = cand.toTimeZone(tjTz).time(); const QTime searchLocalTime = cand.toTimeZone(tjTz).time();
if (listTime.isValid() && searchLocalTime.isValid()) { if (listTime.isValid() && searchLocalTime.isValid()) {
const int diffSecs = qAbs(listTime.secsTo(searchLocalTime)); int diffSecs = searchLocalTime.secsTo(listTime); // >0 = listTime (tx) позже цели
if (diffSecs > 600 && diffSecs < 85800) continue; // 85800 = 23h50m if (diffSecs < -43200) diffSecs += 86400; // wraparound через полночь вперёд
else if (diffSecs > 43200) diffSecs -= 86400; // wraparound назад
if (diffSecs < 0 || diffSecs > 600) continue; // tx раньше цели или позже неё на >10 мин
} }
} }
@ -915,7 +917,7 @@ void GetLastTransactionsScript::searchAll(
<< "date=" << detail.date << "time=" << detail.time << "date=" << detail.date << "time=" << detail.time
<< "amount=" << detail.amount << "receiver=" << detail.receiverAccount; << "amount=" << detail.amount << "receiver=" << detail.receiverAccount;
// Проверяем время ±10 минут // Проверяем время: txTime не раньше цели и не позже неё на 10 минут (+10м, одностороннее окно)
if (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");
@ -924,9 +926,9 @@ void GetLastTransactionsScript::searchAll(
} }
if (txTime.isValid()) { if (txTime.isValid()) {
txTime.setTimeZone(tjTz); txTime.setTimeZone(tjTz);
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(searchTime.toUTC())); const qint64 diffSecs = searchTime.toUTC().secsTo(txTime.toUTC()); // >0 = txTime позже цели
if (diffSecs > 600) { if (diffSecs < 0 || diffSecs > 600) {
qDebug() << "[Dushanbe::GetLastTransactions] Time mismatch: diff=" << diffSecs << "sec (>10min), skipping"; qDebug() << "[Dushanbe::GetLastTransactions] Time mismatch: diff=" << diffSecs << "sec (out of +10min window), skipping";
AdbUtils::goBack(m_deviceId); AdbUtils::goBack(m_deviceId);
QThread::msleep(1000); QThread::msleep(1000);
continue; continue;
@ -956,9 +958,15 @@ void GetLastTransactionsScript::searchAll(
if (txTime.isValid()) txTime.setTimeZone(tjTz); if (txTime.isValid()) txTime.setTimeZone(tjTz);
QJsonObject txJson; QJsonObject txJson;
txJson["bank_transaction_id"] = detail.operationId.isEmpty() // Фолбэк-id (когда «Номер операции» не распарсился) строим из
? QString("dushanbe_") + detail.date + "_" + detail.time // created_at задачи — он детерминирован и стабилен между запусками,
: detail.operationId; // что важно для дедупликации на сервере. Раньше брали распарсенные
// с экрана дату+время, которые могли «плавать» из-за OCR.
txJson["bank_transaction_id"] = !detail.operationId.isEmpty()
? detail.operationId
: (searchTime.isValid()
? QStringLiteral("dushanbe_") + searchTime.toUTC().toString(Qt::ISODate)
: QStringLiteral("dushanbe_") + detail.date + "_" + detail.time);
// Списание хранится с отрицательным знаком, пополнение — положительным. // Списание хранится с отрицательным знаком, пополнение — положительным.
// detail.amount всегда положителен (OCR), знак берём из списка Выписки. // detail.amount всегда положителен (OCR), знак берём из списка Выписки.
const double signedAmount = (tx.amount < 0 ? -1.0 : 1.0) * qAbs(finalAmount); const double signedAmount = (tx.amount < 0 ? -1.0 : 1.0) * qAbs(finalAmount);
@ -971,7 +979,11 @@ void GetLastTransactionsScript::searchAll(
QJsonObject extraObj; QJsonObject extraObj;
extraObj["fee"] = static_cast<int>(finalFee * 100); extraObj["fee"] = static_cast<int>(finalFee * 100);
txJson["extra"] = extraObj; txJson["extra"] = extraObj;
txJson["created_at"] = txTime.isValid() // created_at — время из задачи (поисковый target), real_created_at —
// фактическое время транзакции, спарсенное с экрана приложения.
txJson["created_at"] = searchTime.isValid()
? searchTime.toUTC().toString(Qt::ISODate) : QJsonValue(QJsonValue::Null);
txJson["real_created_at"] = txTime.isValid()
? txTime.toUTC().toString(Qt::ISODate) : QJsonValue(QJsonValue::Null); ? txTime.toUTC().toString(Qt::ISODate) : QJsonValue(QJsonValue::Null);
qDebug() << "[Dushanbe::GetLastTransactions] Emitting parsed tx:" qDebug() << "[Dushanbe::GetLastTransactions] Emitting parsed tx:"

View File

@ -618,10 +618,10 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
} }
} }
// Проверяем время (±10 минут) // Проверяем время: txTime не раньше цели и не позже неё на 10 минут (+10м, одностороннее окно)
if (txTime.isValid() && searchTime.isValid()) { if (txTime.isValid() && searchTime.isValid()) {
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(searchTime.toUTC())); const qint64 diffSecs = searchTime.toUTC().secsTo(txTime.toUTC()); // >0 = txTime позже цели
if (diffSecs > 600) { if (diffSecs < 0 || 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")
<< "diff=" << diffSecs << "sec"; << "diff=" << diffSecs << "sec";
@ -644,6 +644,16 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
QString externalId = detail.bankTrExternalId; QString externalId = detail.bankTrExternalId;
// Фолбэк-id (когда реальный ID банка не распарсился) строим из
// created_at задачи — он детерминирован и стабилен между запусками,
// что важно для дедупликации на сервере. Раньше брали распарсенное
// с экрана время (dtStr), которое могло «плавать» из-за OCR.
const QString finalExternalId = externalId.isEmpty()
? QStringLiteral("ozon_") + (searchTime.isValid()
? searchTime.toUTC().toString(Qt::ISODate)
: QString(dtStr).remove(',').trimmed().replace(' ', '_'))
: externalId;
// Сохраняем // Сохраняем
if (accountId != -1) { if (accountId != -1) {
TransactionInfo tx; TransactionInfo tx;
@ -651,9 +661,7 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
tx.amount = parsed.amount; tx.amount = parsed.amount;
tx.bankName = m_appCode; tx.bankName = m_appCode;
tx.bankTime = dtStr; tx.bankTime = dtStr;
tx.bankTrExternalId = externalId.isEmpty() tx.bankTrExternalId = finalExternalId;
? QString("ozon_") + QString(dtStr).remove(',').trimmed().replace(' ', '_')
: externalId;
tx.status = TransactionStatus::Complete; tx.status = TransactionStatus::Complete;
tx.timestamp = DateUtils::getUtcNow(); tx.timestamp = DateUtils::getUtcNow();
if (txTime.isValid()) tx.completeTime = txTime; if (txTime.isValid()) tx.completeTime = txTime;
@ -671,10 +679,6 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
// Отдаём результат в EventHandler через сигнал (как Dushanbe) // Отдаём результат в EventHandler через сигнал (как Dushanbe)
{ {
const QString finalExternalId = externalId.isEmpty()
? QString("ozon_") + QString(dtStr).remove(',').trimmed().replace(' ', '_')
: externalId;
QJsonObject txJson; QJsonObject txJson;
txJson["bank_transaction_id"] = finalExternalId; txJson["bank_transaction_id"] = finalExternalId;
txJson["amount"] = static_cast<int>(parsed.amount * 100); txJson["amount"] = static_cast<int>(parsed.amount * 100);
@ -687,7 +691,11 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
QJsonObject extraObj; QJsonObject extraObj;
extraObj["fee"] = 0; extraObj["fee"] = 0;
txJson["extra"] = extraObj; txJson["extra"] = extraObj;
txJson["created_at"] = txTime.isValid() // created_at — время из задачи (поисковый target), real_created_at —
// фактическое время транзакции, спарсенное с экрана приложения.
txJson["created_at"] = searchTime.isValid()
? searchTime.toUTC().toString(Qt::ISODate) : QJsonValue(QJsonValue::Null);
txJson["real_created_at"] = txTime.isValid()
? txTime.toUTC().toString(Qt::ISODate) : QJsonValue(QJsonValue::Null); ? txTime.toUTC().toString(Qt::ISODate) : QJsonValue(QJsonValue::Null);
qDebug() << "[FetchTransactions] Emitting parsed tx:" qDebug() << "[FetchTransactions] Emitting parsed tx:"

View File

@ -21,6 +21,8 @@ pathDevice=/api/v1/device/
pathDeviceScreenshot=/api/v1/device/add_screenshot/ pathDeviceScreenshot=/api/v1/device/add_screenshot/
pathMaterial=/api/v1/profile/material pathMaterial=/api/v1/profile/material
pathProfilePing=/api/v1/profile/ping pathProfilePing=/api/v1/profile/ping
pathGetTasks=/api/v1/task/get_tasks
pathTaskResult=/api/v1/task/task_result
pathMonitoringLog=/monitoring/log pathMonitoringLog=/monitoring/log
pathMonitoringDump=/monitoring/dump pathMonitoringDump=/monitoring/dump
pathMonitoringScreenshot=/monitoring/screenshot pathMonitoringScreenshot=/monitoring/screenshot

View File

@ -35,7 +35,7 @@
#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, bool updateStatus = true); 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 = {}, int accountId = -1); 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);
EventHandler::EventHandler(QObject *parent) : QObject(parent) {} EventHandler::EventHandler(QObject *parent) : QObject(parent) {}
@ -79,7 +79,7 @@ void EventHandler::start() {
if (e.sentToServer) continue; if (e.sentToServer) continue;
EventDAO::updateEvent(e.id, EventStatus::Error, "App restarted"); EventDAO::updateEvent(e.id, EventStatus::Error, "App restarted");
EventDAO::markSentToServer(e.id); EventDAO::markSentToServer(e.id);
sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName, "App restarted, event was pending", {}, e.id, e.transactionId, e.accountId); sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName, "App restarted, event was pending", {}, e.id, e.transactionId, e.accountId, e.json);
qDebug() << "[EventHandler] Closed stale event id:" << e.id; qDebug() << "[EventHandler] Closed stale event id:" << e.id;
} }
} }
@ -120,7 +120,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
const MaterialInfo account = MaterialDAO::findByMaterialId(materialId); const MaterialInfo account = MaterialDAO::findByMaterialId(materialId);
if (account.id == -1) { if (account.id == -1) {
qWarning() << "[EventHandler] Account not found for bank:" << bankName << "material_id:" << materialId; qWarning() << "[EventHandler] Account not found for bank:" << bankName << "material_id:" << materialId;
sendTaskError(typeStr, materialId, bankName, "Account not found for material_id: " + materialId, {}, -1, incomingTransactionId); sendTaskError(typeStr, materialId, bankName, "Account not found for material_id: " + materialId, {}, -1, incomingTransactionId, -1, QString::fromUtf8(QJsonDocument(task).toJson(QJsonDocument::Compact)));
continue; continue;
} }
@ -128,7 +128,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
if (eventType == EventType::FetchProfile) { if (eventType == EventType::FetchProfile) {
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByMaterial(materialId, EventType::FetchProfile); const QList<EventInfo> cancelled = EventDAO::cancelWaitingByMaterial(materialId, EventType::FetchProfile);
for (const EventInfo &c : cancelled) { for (const EventInfo &c : cancelled) {
sendTaskError(eventTypeToString(c.type), c.externalId, c.bankName, "Cancelled: replaced by new task", {}, c.id, c.transactionId, c.accountId); sendTaskError(eventTypeToString(c.type), c.externalId, c.bankName, "Cancelled: replaced by new task", {}, c.id, c.transactionId, c.accountId, c.json);
EventDAO::markSentToServer(c.id); EventDAO::markSentToServer(c.id);
} }
} }
@ -169,7 +169,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
if (!onlineDevices.contains(account.deviceId)) { if (!onlineDevices.contains(account.deviceId)) {
qWarning() << "[EventHandler] Device offline for material_id:" << materialId << "device:" << account.deviceId; qWarning() << "[EventHandler] Device offline for material_id:" << materialId << "device:" << account.deviceId;
EventDAO::updateEvent(eventId, EventStatus::Error, "Device is offline"); EventDAO::updateEvent(eventId, EventStatus::Error, "Device is offline");
sendTaskError(typeStr, materialId, bankName, "Device is offline", {}, eventId, incomingTransactionId, account.id); sendTaskError(typeStr, materialId, bankName, "Device is offline", {}, eventId, incomingTransactionId, account.id, event.json);
} }
} }
@ -216,7 +216,7 @@ static void sendTaskResult(const QString &type, const QString &materialId, const
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName,
const QString &description, const QByteArray &screenshot, int eventId, const QString &description, const QByteArray &screenshot, int eventId,
const QString &transactionId, int accountId) { const QString &transactionId, int accountId, const QString &taskJson) {
QJsonObject errorData; QJsonObject errorData;
errorData["status"] = QStringLiteral("error"); errorData["status"] = QStringLiteral("error");
errorData["event_type"] = type; errorData["event_type"] = type;
@ -261,6 +261,15 @@ static void sendTaskError(const QString &type, const QString &materialId, const
if (!appVersion.isEmpty()) if (!appVersion.isEmpty())
lines << QStringLiteral("AppVersion: %1").arg(appVersion.toHtmlEscaped()); lines << QStringLiteral("AppVersion: %1").arg(appVersion.toHtmlEscaped());
lines << QStringLiteral("Error: %1").arg(description.toHtmlEscaped()); lines << QStringLiteral("Error: %1").arg(description.toHtmlEscaped());
// JSON задачи, пришедшей с сервера (pretty-print). НЕ экранируем — админка
// (_render_msg) экранирует один раз и выносит блок после «JSON: » в <pre>.
if (!taskJson.isEmpty()) {
const QJsonDocument doc = QJsonDocument::fromJson(taskJson.toUtf8());
const QString pretty = doc.isNull()
? taskJson
: QString::fromUtf8(doc.toJson(QJsonDocument::Indented));
lines << QStringLiteral("JSON: %1").arg(pretty);
}
const QString message = lines.join(QStringLiteral("\n")); const QString message = lines.join(QStringLiteral("\n"));
sendMonitoringLog("error", "task_error", message, screenshot, eventId); sendMonitoringLog("error", "task_error", message, screenshot, eventId);
@ -268,6 +277,50 @@ static void sendTaskError(const QString &type, const QString &materialId, const
// чтобы покрывать также non-event-driven задачи (login flow и т.п.). // чтобы покрывать также non-event-driven задачи (login flow и т.п.).
} }
// Успешное завершение задачи в arc-monitoring (мониторинговый дроплет, не apiBase).
// Бизнес-результат уходит отдельно (sendTaskResult / per-tx streaming) — здесь
// только человекочитаемый лог в админку с теми же метаданными, что у task_error
// (Desktop/Profile/Material/Phone/AppVersion), чтобы корректно парсились и
// фильтровались. `summary` — операционные детали (Amount/Found/…).
static void sendTaskSuccess(const QString &type, const QString &materialId, const QString &bankName,
const QString &summary, const QByteArray &screenshot, int eventId,
int accountId) {
QString profileUuid, profilePhone, appVersion;
if (accountId > 0) {
const MaterialInfo acc = MaterialDAO::getAccountById(accountId);
if (acc.id != -1) {
const BankProfileInfo app = BankProfileDAO::getApplication(acc.deviceId, acc.appCode);
profileUuid = app.bankProfileId;
profilePhone = app.phone;
appVersion = app.appVersion;
}
}
const QString desktopId = SettingsDAO::get("desktop_id");
const QString clientVersion =
QSettings("config.ini", QSettings::IniFormat).value("common/version").toString();
QStringList lines;
lines << QStringLiteral("<b>[TASK_SUCCESS] [%1] [%2]</b>")
.arg(type.toHtmlEscaped(), bankName.toUpper().toHtmlEscaped());
if (!desktopId.isEmpty())
lines << QStringLiteral("Desktop: %1").arg(desktopId.toHtmlEscaped());
if (!clientVersion.isEmpty())
lines << QStringLiteral("DesktopVersion: %1").arg(clientVersion.toHtmlEscaped());
if (!profileUuid.isEmpty())
lines << QStringLiteral("Profile: %1").arg(profileUuid.toHtmlEscaped());
if (!materialId.isEmpty())
lines << QStringLiteral("Material: %1").arg(materialId.toHtmlEscaped());
if (!profilePhone.isEmpty())
lines << QStringLiteral("Phone: %1").arg(profilePhone.toHtmlEscaped());
if (!appVersion.isEmpty())
lines << QStringLiteral("AppVersion: %1").arg(appVersion.toHtmlEscaped());
// summary содержит JSON — НЕ экранируем: админка (_render_msg) экранирует
// ровно один раз сама. Двойной toHtmlEscaped превратил бы " в &quot; в тексте.
if (!summary.isEmpty())
lines << summary;
const QString message = lines.join(QStringLiteral("\n"));
sendMonitoringLog("info", "task_success", message, screenshot, eventId, /*toApiBase=*/false);
}
static void sendMonitoringLog(const QString &type, const QString &event, const QString &message, static void sendMonitoringLog(const QString &type, const QString &event, const QString &message,
const QByteArray &image, int eventId, bool toApiBase) { const QByteArray &image, int eventId, bool toApiBase) {
auto *thread = new QThread; auto *thread = new QThread;
@ -327,6 +380,7 @@ bool EventHandler::tryRetry(const QString &key, const EventInfo &event, const QS
// счётчик отправленных в 0. // счётчик отправленных в 0.
m_screenshots.remove(key); m_screenshots.remove(key);
m_parsedTxCount.remove(key); m_parsedTxCount.remove(key);
m_parsedTxJson.remove(key);
// Снимаем текущий поток/таймер (в timeout-ветке они уже сняты — take() вернёт null) // Снимаем текущий поток/таймер (в timeout-ветке они уже сняты — take() вернёт null)
if (const auto tm = m_timers.take(key)) { if (const auto tm = m_timers.take(key)) {
@ -397,7 +451,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
.arg(eventTypeToString(event.type), deviceLabel, event.bankName, result); .arg(eventTypeToString(event.type), deviceLabel, event.bankName, result);
GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg, screenshot); GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg, screenshot);
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result, screenshot, event.id, event.transactionId, event.accountId); sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result, screenshot, event.id, event.transactionId, event.accountId, event.json);
} else { } else {
qDebug() << "[EventHandler] Interrupted — server error report skipped (cause already reported by interrupter), event id:" << event.id; qDebug() << "[EventHandler] Interrupted — server error report skipped (cause already reported by interrupter), event id:" << event.id;
} }
@ -445,6 +499,9 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
EventDAO::markSentToServer(event.id); EventDAO::markSentToServer(event.id);
} }
// JSON результата, ушедшего на apiBase — кладём в текст task_success ниже.
QString apiResultJson;
if (newStatus != EventStatus::Error) { if (newStatus != EventStatus::Error) {
const MaterialInfo account = MaterialDAO::getAccountById(event.accountId); const MaterialInfo account = MaterialDAO::getAccountById(event.accountId);
const QMap<QString, int> currencyCodes = { const QMap<QString, int> currencyCodes = {
@ -505,6 +562,8 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
obj["extra"] = extraObj; obj["extra"] = extraObj;
taskData = obj; taskData = obj;
apiResultJson = QString::fromUtf8(
QJsonDocument(obj).toJson(QJsonDocument::Indented));
} else { } else {
QJsonObject obj; QJsonObject obj;
obj["status"] = "completed"; obj["status"] = "completed";
@ -528,8 +587,10 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial; const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial;
if (event.type == EventType::CreateTransaction) { if (event.type == EventType::CreateTransaction) {
const QString msg = QStringLiteral("[CREATE_TRANSACTION] bank=%1 material=%2 amount=%3 phone=%4") QString msg = QStringLiteral("[CREATE_TRANSACTION] bank=%1 material=%2 amount=%3 phone=%4")
.arg(event.bankName, event.externalId, QString::number(event.amount, 'f', 2), event.phone); .arg(event.bankName, event.externalId, QString::number(event.amount, 'f', 2), event.phone);
if (!apiResultJson.isEmpty())
msg += QStringLiteral("\nJSON: %1").arg(apiResultJson);
QByteArray screenshot; QByteArray screenshot;
const QString tmpPath = QDir::tempPath() + "/arc_tx_" + QString::number(event.id); const QString tmpPath = QDir::tempPath() + "/arc_tx_" + QString::number(event.id);
if (AdbUtils::takeScreenshot(event.deviceId, tmpPath)) { if (AdbUtils::takeScreenshot(event.deviceId, tmpPath)) {
@ -541,6 +602,14 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
f.remove(); f.remove();
} }
sendMonitoringLog("info", "task_success", msg, screenshot, event.id, /*toApiBase=*/true); sendMonitoringLog("info", "task_success", msg, screenshot, event.id, /*toApiBase=*/true);
// Дублируем успех в arc-monitoring (см. sendTaskSuccess) — туда же,
// куда уходят ошибки, чтобы видеть и удачные создания транзакций.
QString createSummary = QStringLiteral("Amount: %1\nRecipient: %2")
.arg(QString::number(event.amount, 'f', 2), event.phone);
if (!apiResultJson.isEmpty())
createSummary += QStringLiteral("\nJSON: %1").arg(apiResultJson);
sendTaskSuccess("CREATE_TRANSACTION", event.externalId, event.bankName,
createSummary, screenshot, event.id, event.accountId);
// Декрементируем материальный/профильный баланс на сумму расхода + комиссию. // Декрементируем материальный/профильный баланс на сумму расхода + комиссию.
// Между запусками FETCH_PROFILE это держит UI/локальный баланс // Между запусками FETCH_PROFILE это держит UI/локальный баланс
@ -586,10 +655,22 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
} }
if (event.type == EventType::FetchTransactions) { if (event.type == EventType::FetchTransactions) {
const QString msg = QStringLiteral("[FETCH_TRANSACTIONS] bank=%1 material=%2") // JSON для текста: массив транзакций, отправленных потоково на apiBase.
.arg(event.bankName, event.externalId); const QJsonArray txArr = m_parsedTxJson.value(key);
const QString txJson = QString::fromUtf8(
QJsonDocument(txArr).toJson(QJsonDocument::Indented));
QString msg = QStringLiteral("[FETCH_TRANSACTIONS] bank=%1 material=%2 found=%3")
.arg(event.bankName, event.externalId, QString::number(txArr.size()));
if (!txArr.isEmpty())
msg += QStringLiteral("\nJSON: %1").arg(txJson);
const QByteArray scr = m_screenshots.take(key); const QByteArray scr = m_screenshots.take(key);
sendMonitoringLog("info", "task_success", msg, scr, event.id, /*toApiBase=*/true); sendMonitoringLog("info", "task_success", msg, scr, event.id, /*toApiBase=*/true);
// Дублируем успех в arc-monitoring с теми же метаданными, что у ошибок.
QString fetchSummary = QStringLiteral("Found: %1").arg(txArr.size());
if (!txArr.isEmpty())
fetchSummary += QStringLiteral("\nJSON: %1").arg(txJson);
sendTaskSuccess("FETCH_TRANSACTIONS", event.externalId, event.bankName,
fetchSummary, scr, event.id, event.accountId);
} }
if (event.type == EventType::FetchProfile) { if (event.type == EventType::FetchProfile) {
@ -608,6 +689,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); m_parsedTxCount.remove(key);
m_parsedTxJson.remove(key);
// Убиваем банковское приложение на устройстве после завершения задачи // Убиваем банковское приложение на устройстве после завершения задачи
{ {
@ -707,6 +789,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
// ретраить уже частично отработанную задачу. // ретраить уже частично отработанную задачу.
auto streamParsedTx = [this, key, event](const QJsonObject &tx) { auto streamParsedTx = [this, key, event](const QJsonObject &tx) {
m_parsedTxCount[key] = m_parsedTxCount.value(key, 0) + 1; m_parsedTxCount[key] = m_parsedTxCount.value(key, 0) + 1;
m_parsedTxJson[key].append(tx); // копим для показа JSON в task_success
QJsonArray arr; QJsonArray arr;
arr.append(tx); arr.append(tx);
sendTaskResult(eventTypeToString(EventType::FetchTransactions), sendTaskResult(eventTypeToString(EventType::FetchTransactions),
@ -796,7 +879,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
const QString logMsg = QString("[%1] bank=%2 material=%3: Script timeout") const QString logMsg = QString("[%1] bank=%2 material=%3: Script timeout")
.arg(eventTypeToString(event.type), event.bankName, event.externalId); .arg(eventTypeToString(event.type), event.bankName, event.externalId);
GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg); GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg);
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, timeoutMsg, {}, event.id, event.transactionId, event.accountId); sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, timeoutMsg, {}, event.id, event.transactionId, event.accountId, event.json);
EventDAO::markSentToServer(event.id); EventDAO::markSentToServer(event.id);
// Убиваем банковское приложение на устройстве после таймаута // Убиваем банковское приложение на устройстве после таймаута
@ -833,7 +916,7 @@ void EventHandler::processNextTask(const QString &deviceId) {
if (waitSecs > 5 * 60) { if (waitSecs > 5 * 60) {
qWarning() << "[EventHandler] Task expired after" << waitSecs << "s, event id:" << event.id; qWarning() << "[EventHandler] Task expired after" << waitSecs << "s, event id:" << event.id;
EventDAO::updateEvent(event.id, EventStatus::Error, "Task expired: waited " + QString::number(waitSecs) + "s"); EventDAO::updateEvent(event.id, EventStatus::Error, "Task expired: waited " + QString::number(waitSecs) + "s");
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Task expired: not started within 5 minutes", {}, event.id, event.transactionId, event.accountId); sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Task expired: not started within 5 minutes", {}, event.id, event.transactionId, event.accountId, event.json);
continue; continue;
} }
@ -843,7 +926,7 @@ void EventHandler::processNextTask(const QString &deviceId) {
qWarning() << "[EventHandler] Bank profile disabled for" << account.appCode << "device:" << deviceId; qWarning() << "[EventHandler] Bank profile disabled for" << account.appCode << "device:" << deviceId;
EventDAO::updateEvent(event.id, EventStatus::Error, "Bank profile is disabled"); EventDAO::updateEvent(event.id, EventStatus::Error, "Bank profile is disabled");
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName,
"Bank profile is disabled: " + account.appCode, {}, event.id, event.transactionId, event.accountId); "Bank profile is disabled: " + account.appCode, {}, event.id, event.transactionId, event.accountId, event.json);
continue; continue;
} }
@ -886,6 +969,7 @@ void EventHandler::clearAll() {
m_timers.clear(); m_timers.clear();
m_screenshots.clear(); m_screenshots.clear();
m_parsedTxCount.clear(); m_parsedTxCount.clear();
m_parsedTxJson.clear();
} }
void EventHandler::stop() { void EventHandler::stop() {

View File

@ -35,6 +35,7 @@ private:
QMap<QString, QTimer *> m_timers; QMap<QString, QTimer *> m_timers;
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<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;