1
0
forked from BRT/arc

Add bank profile and phone metadata for improved task tracking and error reporting. Update TaskDumpMeta structure and associated scripts to include bankProfileId and profilePhone. Enhance LogArchiveSender and monitoring outputs for better operator identification and debugging.

This commit is contained in:
slava 2026-05-30 14:54:32 +05:00
parent 65d3b00bb9
commit 8d86d5f835
20 changed files with 102 additions and 50 deletions

View File

@ -37,7 +37,7 @@ void GetCardInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("black", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("black", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран // Читаем текущий экран

View File

@ -35,7 +35,7 @@ void GetProfileInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран // Читаем текущий экран

View File

@ -36,7 +36,7 @@ void LoginAndCheckAccountsScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -166,22 +166,30 @@ TaskDumpRecorder::TaskDumpRecorder(const QString &bank, const QString &taskKind,
} }
QString TaskDumpRecorder::buildCaption(const QString &errorMessage) const { QString TaskDumpRecorder::buildCaption(const QString &errorMessage) const {
const QString apiToken = SettingsDAO::get("api_key");
const QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate); const QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate);
// api-токен в caption больше не пишем: он общий на нескольких операторов и
// не позволяет отличить, чей профиль упал. Идентифицируем по UUID профиля,
// материалу и телефону профиля (см. m_meta).
QStringList head{QStringLiteral("[TASK_ERROR]")}; QStringList head{QStringLiteral("[TASK_ERROR]")};
if (!apiToken.isEmpty())
head << QStringLiteral("[%1]").arg(apiToken.toHtmlEscaped());
head << QStringLiteral("[%1]").arg(m_bank.toHtmlEscaped()); head << QStringLiteral("[%1]").arg(m_bank.toHtmlEscaped());
head << QStringLiteral("[%1]").arg(m_taskKind.toHtmlEscaped()); head << QStringLiteral("[%1]").arg(m_taskKind.toHtmlEscaped());
QString body = QStringLiteral("Device: %1").arg(m_adbDeviceIdRaw.toHtmlEscaped()); const QString desktopId = SettingsDAO::get("desktop_id");
QString body;
if (!desktopId.isEmpty())
body += QStringLiteral("Desktop: %1\n").arg(desktopId.toHtmlEscaped());
body += QStringLiteral("Device: %1").arg(m_adbDeviceIdRaw.toHtmlEscaped());
if (!m_meta.bankProfileId.isEmpty())
body += QStringLiteral("\nProfile: %1").arg(m_meta.bankProfileId.toHtmlEscaped());
if (!m_meta.materialId.isEmpty()) if (!m_meta.materialId.isEmpty())
body += QStringLiteral("\nMaterial: %1").arg(m_meta.materialId.toHtmlEscaped()); body += QStringLiteral("\nMaterial: %1").arg(m_meta.materialId.toHtmlEscaped());
if (!m_meta.profilePhone.isEmpty())
body += QStringLiteral("\nPhone: %1").arg(m_meta.profilePhone.toHtmlEscaped());
if (m_meta.amount != 0) if (m_meta.amount != 0)
body += QStringLiteral("\nAmount: %1").arg(static_cast<qint64>(m_meta.amount)); body += QStringLiteral("\nAmount: %1").arg(static_cast<qint64>(m_meta.amount));
if (!m_meta.phone.isEmpty()) if (!m_meta.phone.isEmpty())
body += QStringLiteral("\nPhone: %1").arg(m_meta.phone.toHtmlEscaped()); body += QStringLiteral("\nRecipient: %1").arg(m_meta.phone.toHtmlEscaped());
if (!m_meta.card.isEmpty()) if (!m_meta.card.isEmpty())
body += QStringLiteral("\nCard: %1").arg(m_meta.card.toHtmlEscaped()); body += QStringLiteral("\nCard: %1").arg(m_meta.card.toHtmlEscaped());
body += QStringLiteral("\nError: %1").arg(errorMessage.left(800).toHtmlEscaped()); body += QStringLiteral("\nError: %1").arg(errorMessage.left(800).toHtmlEscaped());
@ -238,9 +246,13 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
if (ef.open(QIODevice::WriteOnly | QIODevice::Truncate)) { if (ef.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QTextStream ts(&ef); QTextStream ts(&ef);
ts.setEncoding(QStringConverter::Utf8); ts.setEncoding(QStringConverter::Utf8);
const QString desktopId = SettingsDAO::get("desktop_id");
if (!desktopId.isEmpty()) ts << "desktop_id: " << desktopId << "\n";
ts << "bank: " << m_bank << "\n" ts << "bank: " << m_bank << "\n"
<< "task: " << m_taskKind << "\n" << "task: " << m_taskKind << "\n"
<< "device: " << m_adbDeviceIdRaw << "\n"; << "device: " << m_adbDeviceIdRaw << "\n";
if (!m_meta.bankProfileId.isEmpty()) ts << "profile_id: " << m_meta.bankProfileId << "\n";
if (!m_meta.profilePhone.isEmpty()) ts << "profile_phone: " << m_meta.profilePhone << "\n";
if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n"; if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n";
if (m_meta.amount != 0) ts << "amount: " << static_cast<qint64>(m_meta.amount) << "\n"; if (m_meta.amount != 0) ts << "amount: " << static_cast<qint64>(m_meta.amount) << "\n";
if (!m_meta.phone.isEmpty()) ts << "phone: " << m_meta.phone << "\n"; if (!m_meta.phone.isEmpty()) ts << "phone: " << m_meta.phone << "\n";
@ -255,6 +267,17 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
const QString archivePath = m_folder + ".zip"; const QString archivePath = m_folder + ".zip";
const QString text = buildCaption(errorMessage); const QString text = buildCaption(errorMessage);
// Тот же идентификатор профиля/материала/телефона добавляем в caption архива
// логов ([LOGS]), чтобы по общему api-токену можно было разделить операторов.
QStringList logsIdLines;
if (!m_meta.bankProfileId.isEmpty())
logsIdLines << QStringLiteral("Profile: %1").arg(m_meta.bankProfileId.toHtmlEscaped());
if (!m_meta.materialId.isEmpty())
logsIdLines << QStringLiteral("Material: %1").arg(m_meta.materialId.toHtmlEscaped());
if (!m_meta.profilePhone.isEmpty())
logsIdLines << QStringLiteral("Phone: %1").arg(m_meta.profilePhone.toHtmlEscaped());
const QString logsIdentity = logsIdLines.join(QStringLiteral("\n"));
// Финальный скриншот шлём отдельным полем `image`, поэтому сразу читаем // Финальный скриншот шлём отдельным полем `image`, поэтому сразу читаем
// его в память, пока папка ещё цела. // его в память, пока папка ещё цела.
QByteArray finalScreenshot; QByteArray finalScreenshot;
@ -265,7 +288,7 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
} }
} }
QThread *bg = QThread::create([folder, archivePath, text, finalScreenshot]() { QThread *bg = QThread::create([folder, archivePath, text, finalScreenshot, logsIdentity]() {
QByteArray archiveBytes; QByteArray archiveBytes;
if (zipFolder(folder, archivePath)) { if (zipFolder(folder, archivePath)) {
archiveBytes = readFileBytes(archivePath); archiveBytes = readFileBytes(archivePath);
@ -301,7 +324,7 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
// Следом — application.log + БД (+ WAL/SHM) отдельным архивом. // Следом — application.log + БД (+ WAL/SHM) отдельным архивом.
// Запускаем уже ПОСЛЕ того, как task-дамп поставлен в очередь // Запускаем уже ПОСЛЕ того, как task-дамп поставлен в очередь
// отправки, чтобы порядок прихода на relay был "дамп → логи". // отправки, чтобы порядок прихода на relay был "дамп → логи".
LogArchiveSender::dispatch(QStringLiteral("TASK_ERROR")); LogArchiveSender::dispatch(QStringLiteral("TASK_ERROR"), -1, logsIdentity);
}); });
QObject::connect(bg, &QThread::finished, bg, &QObject::deleteLater); QObject::connect(bg, &QThread::finished, bg, &QObject::deleteLater);
bg->start(); bg->start();

View File

@ -9,8 +9,10 @@ class NetworkService;
struct TaskDumpMeta { struct TaskDumpMeta {
QString materialId; QString materialId;
double amount = 0; // 0 = нет double amount = 0; // 0 = нет
QString phone; // пусто = нет QString phone; // пусто = нет (телефон получателя в транзакции)
QString card; // пусто = нет QString card; // пусто = нет
QString bankProfileId; // серверный UUID банковского профиля (детект в общих логах)
QString profilePhone; // телефон банковского профиля (не получателя)
}; };
class TaskDumpRecorder { class TaskDumpRecorder {

View File

@ -36,7 +36,7 @@ void GetCardInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -276,7 +276,7 @@ void GetLastTransactionsScript::doStart() {
} }
const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first; const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first;
const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}}; const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -39,7 +39,7 @@ void GetProfileInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -41,7 +41,7 @@ void LoginAndCheckAccountsScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -123,7 +123,7 @@ void PayByCardScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber}; const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));

View File

@ -120,7 +120,7 @@ void PayByPhoneScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}}; const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_PHONE", device.adbSerial, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_PHONE", device.adbSerial, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));

View File

@ -42,7 +42,7 @@ void GetCardInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран // Читаем текущий экран

View File

@ -109,7 +109,7 @@ void GetLastTransactionsScript::doStart() {
} }
const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first; const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first;
const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}}; const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
if (!navigateToTransactionList(device, app)) { if (!navigateToTransactionList(device, app)) {

View File

@ -37,7 +37,7 @@ void GetProfileInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран // Читаем текущий экран

View File

@ -40,7 +40,7 @@ void LoginAndCheckAccountsScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -91,7 +91,7 @@ void PayByCardScript::doStart() {
} }
const QString &adbId = device.adbSerial; const QString &adbId = device.adbSerial;
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber}; const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_CARD", adbId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_CARD", adbId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));

View File

@ -100,7 +100,7 @@ void PayByPhoneScript::doStart() {
} }
const QString &adbId = device.adbSerial; const QString &adbId = device.adbSerial;
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}}; const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));

View File

@ -34,7 +34,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 = {}); 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 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) {}
@ -78,7 +78,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", {}, -1, e.transactionId); sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName, "App restarted, event was pending", {}, -1, e.transactionId, e.accountId);
qDebug() << "[EventHandler] Closed stale event id:" << e.id; qDebug() << "[EventHandler] Closed stale event id:" << e.id;
} }
} }
@ -127,7 +127,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", {}, -1, c.transactionId); sendTaskError(eventTypeToString(c.type), c.externalId, c.bankName, "Cancelled: replaced by new task", {}, -1, c.transactionId, c.accountId);
EventDAO::markSentToServer(c.id); EventDAO::markSentToServer(c.id);
} }
} }
@ -168,7 +168,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", {}, -1, incomingTransactionId); sendTaskError(typeStr, materialId, bankName, "Device is offline", {}, -1, incomingTransactionId, account.id);
} }
} }
@ -215,7 +215,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) { const QString &transactionId, int accountId) {
QJsonObject errorData; QJsonObject errorData;
errorData["status"] = QStringLiteral("error"); errorData["status"] = QStringLiteral("error");
errorData["event_type"] = type; errorData["event_type"] = type;
@ -224,18 +224,36 @@ static void sendTaskError(const QString &type, const QString &materialId, const
errorData["transaction_id"] = transactionId; errorData["transaction_id"] = transactionId;
sendTaskResult(QStringLiteral("ERROR"), materialId, bankName, errorData, eventId); sendTaskResult(QStringLiteral("ERROR"), materialId, bankName, errorData, eventId);
if (!screenshot.isEmpty()) { // Шлём в мониторинг всегда — даже без скриншота (offline / expired /
const QString apiToken = SettingsDAO::get("api_key"); // disabled / account not found и т.п.), чтобы такие ошибки не «молчали» на
// релее. Скриншот прикладываем, если он есть.
// api-токен общий на нескольких операторов — по нему не отличить, чей
// профиль упал. Резолвим банковский профиль по accountId и пишем его
// серверный UUID + материал + телефон профиля.
QString profileUuid, profilePhone;
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;
}
}
const QString desktopId = SettingsDAO::get("desktop_id");
QStringList lines; QStringList lines;
lines << QStringLiteral("<b>[TASK_ERROR] [%1] [%2]</b>") lines << QStringLiteral("<b>[TASK_ERROR] [%1] [%2]</b>")
.arg(type.toHtmlEscaped(), bankName.toUpper().toHtmlEscaped()); .arg(type.toHtmlEscaped(), bankName.toUpper().toHtmlEscaped());
if (!apiToken.isEmpty()) if (!desktopId.isEmpty())
lines << QStringLiteral("<b>[%1]</b>").arg(apiToken.toHtmlEscaped()); lines << QStringLiteral("Desktop: %1").arg(desktopId.toHtmlEscaped());
lines << QStringLiteral("<b>[%1]</b>").arg(materialId.toHtmlEscaped()); if (!profileUuid.isEmpty())
lines << description.toHtmlEscaped(); 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());
lines << QStringLiteral("Error: %1").arg(description.toHtmlEscaped());
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);
}
// application.log + БД отправляются из TaskDumpRecorder::finishInternal, // application.log + БД отправляются из TaskDumpRecorder::finishInternal,
// чтобы покрывать также non-event-driven задачи (login flow и т.п.). // чтобы покрывать также non-event-driven задачи (login flow и т.п.).
@ -359,7 +377,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); sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result, screenshot, event.id, event.transactionId, event.accountId);
if (event.type == EventType::CreateTransaction) { if (event.type == EventType::CreateTransaction) {
const MaterialInfo declAcc = MaterialDAO::getAccountById(event.accountId); const MaterialInfo declAcc = MaterialDAO::getAccountById(event.accountId);
@ -750,7 +768,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, "Script timeout (5 min)", {}, -1, event.transactionId); sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Script timeout (5 min)", {}, -1, event.transactionId, event.accountId);
EventDAO::markSentToServer(event.id); EventDAO::markSentToServer(event.id);
// Убиваем банковское приложение на устройстве после таймаута // Убиваем банковское приложение на устройстве после таймаута
@ -787,7 +805,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", {}, -1, event.transactionId); sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Task expired: not started within 5 minutes", {}, -1, event.transactionId, event.accountId);
continue; continue;
} }
@ -797,7 +815,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, {}, -1, event.transactionId); "Bank profile is disabled: " + account.appCode, {}, -1, event.transactionId, event.accountId);
continue; continue;
} }

View File

@ -90,16 +90,22 @@ QByteArray buildArchive() {
} // namespace } // namespace
bool LogArchiveSender::dispatch(const QString &label, int eventId) { bool LogArchiveSender::dispatch(const QString &label, int eventId, const QString &extraCaptionHtml) {
const QString apiToken = SettingsDAO::get("api_key"); const QString apiToken = SettingsDAO::get("api_key");
const QDateTime now = QDateTime::currentDateTime(); const QDateTime now = QDateTime::currentDateTime();
const QString timestamp = now.toString(Qt::ISODate); const QString timestamp = now.toString(Qt::ISODate);
// api-токен в caption больше не пишем (общий на нескольких операторов);
// идентификация идёт по Profile/Material/Phone из extraCaptionHtml.
const QString desktopId = SettingsDAO::get("desktop_id");
QStringList head{QStringLiteral("[LOGS]")}; QStringList head{QStringLiteral("[LOGS]")};
if (!apiToken.isEmpty()) head << QStringLiteral("[%1]").arg(apiToken.toHtmlEscaped());
if (!label.isEmpty()) head << QStringLiteral("[%1]").arg(label.toHtmlEscaped()); if (!label.isEmpty()) head << QStringLiteral("[%1]").arg(label.toHtmlEscaped());
const QString text = QStringLiteral("<b>%1</b>\n%2") QString text = QStringLiteral("<b>%1</b>\n%2")
.arg(head.join(QStringLiteral(" ")), timestamp.toHtmlEscaped()); .arg(head.join(QStringLiteral(" ")), timestamp.toHtmlEscaped());
if (!desktopId.isEmpty())
text += QStringLiteral("\nDesktop: %1").arg(desktopId.toHtmlEscaped());
if (!extraCaptionHtml.isEmpty())
text += QStringLiteral("\n") + extraCaptionHtml;
QString tokenSafe = apiToken; QString tokenSafe = apiToken;
tokenSafe.replace(QRegularExpression(R"([^\w\-.])"), "_"); tokenSafe.replace(QRegularExpression(R"([^\w\-.])"), "_");

View File

@ -10,7 +10,10 @@ namespace LogArchiveSender {
// в собственных background-потоках (worker для zip + thread для HTTP). // в собственных background-потоках (worker для zip + thread для HTTP).
// `label` — короткий тег ("TASK_ERROR" и т.п.), попадает в caption и в имя файла. // `label` — короткий тег ("TASK_ERROR" и т.п.), попадает в caption и в имя файла.
// `eventId` (если > 0) тоже уходит в имя файла для трассировки. // `eventId` (если > 0) тоже уходит в имя файла для трассировки.
// `extraCaptionHtml` (если не пусто) — дополнительные строки caption'а
// (Profile/Material/Phone), уже экранированные; нужны чтобы по общему
// api-токену различать операторов.
// Возвращает true если задача поставлена в очередь; реальный результат отправки // Возвращает true если задача поставлена в очередь; реальный результат отправки
// не отслеживается (fire-and-forget, как у sendMonitoringLog). // не отслеживается (fire-and-forget, как у sendMonitoringLog).
bool dispatch(const QString &label, int eventId = -1); bool dispatch(const QString &label, int eventId = -1, const QString &extraCaptionHtml = {});
} }