Add transactionId support to EventHandler, EventDAO, and related components for improved event tracking and error handling. Update database queries, error reporting, and UI logic accordingly.
This commit is contained in:
parent
9fb209378f
commit
3f9d3eebd0
@ -18,6 +18,7 @@ pathDesktopCreate=/api/v1/desktop/
|
|||||||
pathDevice=/api/v1/device/
|
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
|
||||||
pathMonitoringLog=/monitoring/log
|
pathMonitoringLog=/monitoring/log
|
||||||
pathMonitoringDump=/monitoring/dump
|
pathMonitoringDump=/monitoring/dump
|
||||||
monitoringBase=http://142.93.102.40:8080
|
monitoringBase=http://142.93.102.40:8080
|
||||||
|
|||||||
@ -176,6 +176,7 @@ void DatabaseManager::initSchema() {
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
account_id INTEGER,
|
account_id INTEGER,
|
||||||
external_id TEXT,
|
external_id TEXT,
|
||||||
|
transaction_id TEXT,
|
||||||
device_id TEXT,
|
device_id TEXT,
|
||||||
amount REAL,
|
amount REAL,
|
||||||
phone TEXT,
|
phone TEXT,
|
||||||
@ -192,6 +193,7 @@ void DatabaseManager::initSchema() {
|
|||||||
FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE,
|
FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE
|
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE
|
||||||
))");
|
))");
|
||||||
|
q.exec("ALTER TABLE events ADD COLUMN transaction_id TEXT");
|
||||||
|
|
||||||
q.exec(R"(CREATE TABLE IF NOT EXISTS banks (
|
q.exec(R"(CREATE TABLE IF NOT EXISTS banks (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ EventInfo parseEvent(const QSqlQuery &query) {
|
|||||||
event.status = eventStatusFromString(query.value("status").toString());
|
event.status = eventStatusFromString(query.value("status").toString());
|
||||||
event.type = eventTypeFromString(query.value("type").toString());
|
event.type = eventTypeFromString(query.value("type").toString());
|
||||||
event.externalId = query.value("external_id").toString();
|
event.externalId = query.value("external_id").toString();
|
||||||
|
event.transactionId = query.value("transaction_id").toString();
|
||||||
event.accountId = query.value("account_id").toInt();
|
event.accountId = query.value("account_id").toInt();
|
||||||
event.deviceId = query.value("device_id").toString();
|
event.deviceId = query.value("device_id").toString();
|
||||||
event.amount = query.value("amount").toDouble();
|
event.amount = query.value("amount").toDouble();
|
||||||
@ -36,15 +37,16 @@ int EventDAO::insertEvent(const EventInfo &event) {
|
|||||||
|
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
INSERT INTO events (
|
INSERT INTO events (
|
||||||
status, external_id, account_id, amount, phone, bank_name, comment, json, timestamp, device_id, type
|
status, external_id, transaction_id, account_id, amount, phone, bank_name, comment, json, timestamp, device_id, type
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:status, :external_id, :account_id, :amount, :phone, :bank_name, :comment, :json, :timestamp, :device_id, :type
|
:status, :external_id, :transaction_id, :account_id, :amount, :phone, :bank_name, :comment, :json, :timestamp, :device_id, :type
|
||||||
)
|
)
|
||||||
)");
|
)");
|
||||||
|
|
||||||
query.bindValue(":status", eventStatusToString(event.status));
|
query.bindValue(":status", eventStatusToString(event.status));
|
||||||
query.bindValue(":type", eventTypeToString(event.type));
|
query.bindValue(":type", eventTypeToString(event.type));
|
||||||
query.bindValue(":external_id", event.externalId);
|
query.bindValue(":external_id", event.externalId);
|
||||||
|
query.bindValue(":transaction_id", event.transactionId);
|
||||||
query.bindValue(":account_id", event.accountId);
|
query.bindValue(":account_id", event.accountId);
|
||||||
query.bindValue(":device_id", event.deviceId);
|
query.bindValue(":device_id", event.deviceId);
|
||||||
query.bindValue(":amount", event.amount);
|
query.bindValue(":amount", event.amount);
|
||||||
@ -139,7 +141,7 @@ QList<EventInfo> EventDAO::getAllEvents(const EventStatus &status) {
|
|||||||
|
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
SELECT id, account_id, device_id, external_id, amount, phone, bank_name,
|
SELECT id, account_id, device_id, external_id, transaction_id, amount, phone, bank_name,
|
||||||
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
|
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
|
||||||
FROM events
|
FROM events
|
||||||
WHERE status = :status
|
WHERE status = :status
|
||||||
@ -161,7 +163,7 @@ QList<EventInfo> EventDAO::getAllEvents(const EventStatus &status) {
|
|||||||
EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) {
|
EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) {
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
SELECT id, account_id, device_id, external_id, amount, phone, bank_name,
|
SELECT id, account_id, device_id, external_id, transaction_id, amount, phone, bank_name,
|
||||||
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
|
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
|
||||||
FROM events
|
FROM events
|
||||||
WHERE status = :status AND device_id = :device_id
|
WHERE status = :status AND device_id = :device_id
|
||||||
@ -193,7 +195,7 @@ QList<EventInfo> EventDAO::getEvents(int page, int pageSize) {
|
|||||||
QList<EventInfo> events;
|
QList<EventInfo> events;
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
SELECT id, account_id, device_id, external_id, amount, phone, bank_name,
|
SELECT id, account_id, device_id, external_id, transaction_id, amount, phone, bank_name,
|
||||||
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp,
|
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp,
|
||||||
(LENGTH(screenshot) > 0) as has_screenshot
|
(LENGTH(screenshot) > 0) as has_screenshot
|
||||||
FROM events
|
FROM events
|
||||||
@ -276,7 +278,7 @@ QList<EventInfo> EventDAO::cancelWaitingByMaterial(const QString &materialId, Ev
|
|||||||
QList<EventInfo> cancelled;
|
QList<EventInfo> cancelled;
|
||||||
QSqlQuery selectQuery(DatabaseManager::instance().database());
|
QSqlQuery selectQuery(DatabaseManager::instance().database());
|
||||||
selectQuery.prepare(R"(
|
selectQuery.prepare(R"(
|
||||||
SELECT id, external_id, bank_name, type FROM events
|
SELECT id, external_id, transaction_id, bank_name, type FROM events
|
||||||
WHERE external_id = :material_id AND type = :type AND status = :wait
|
WHERE external_id = :material_id AND type = :type AND status = :wait
|
||||||
)");
|
)");
|
||||||
selectQuery.bindValue(":material_id", materialId);
|
selectQuery.bindValue(":material_id", materialId);
|
||||||
@ -288,8 +290,9 @@ QList<EventInfo> EventDAO::cancelWaitingByMaterial(const QString &materialId, Ev
|
|||||||
EventInfo e;
|
EventInfo e;
|
||||||
e.id = selectQuery.value(0).toInt();
|
e.id = selectQuery.value(0).toInt();
|
||||||
e.externalId = selectQuery.value(1).toString();
|
e.externalId = selectQuery.value(1).toString();
|
||||||
e.bankName = selectQuery.value(2).toString();
|
e.transactionId = selectQuery.value(2).toString();
|
||||||
e.type = eventTypeFromString(selectQuery.value(3).toString());
|
e.bankName = selectQuery.value(3).toString();
|
||||||
|
e.type = eventTypeFromString(selectQuery.value(4).toString());
|
||||||
cancelled.append(e);
|
cancelled.append(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,12 +318,12 @@ QList<EventInfo> EventDAO::cancelWaitingByType(const QString &deviceId, EventTyp
|
|||||||
QSqlQuery selectQuery(DatabaseManager::instance().database());
|
QSqlQuery selectQuery(DatabaseManager::instance().database());
|
||||||
if (bankName.isEmpty()) {
|
if (bankName.isEmpty()) {
|
||||||
selectQuery.prepare(R"(
|
selectQuery.prepare(R"(
|
||||||
SELECT id, external_id, bank_name, type FROM events
|
SELECT id, external_id, transaction_id, bank_name, type FROM events
|
||||||
WHERE device_id = :device_id AND type = :type AND status = :wait
|
WHERE device_id = :device_id AND type = :type AND status = :wait
|
||||||
)");
|
)");
|
||||||
} else {
|
} else {
|
||||||
selectQuery.prepare(R"(
|
selectQuery.prepare(R"(
|
||||||
SELECT id, external_id, bank_name, type FROM events
|
SELECT id, external_id, transaction_id, bank_name, type FROM events
|
||||||
WHERE device_id = :device_id AND type = :type AND status = :wait AND bank_name = :bank_name
|
WHERE device_id = :device_id AND type = :type AND status = :wait AND bank_name = :bank_name
|
||||||
)");
|
)");
|
||||||
selectQuery.bindValue(":bank_name", bankName);
|
selectQuery.bindValue(":bank_name", bankName);
|
||||||
@ -334,8 +337,9 @@ QList<EventInfo> EventDAO::cancelWaitingByType(const QString &deviceId, EventTyp
|
|||||||
EventInfo e;
|
EventInfo e;
|
||||||
e.id = selectQuery.value(0).toInt();
|
e.id = selectQuery.value(0).toInt();
|
||||||
e.externalId = selectQuery.value(1).toString();
|
e.externalId = selectQuery.value(1).toString();
|
||||||
e.bankName = selectQuery.value(2).toString();
|
e.transactionId = selectQuery.value(2).toString();
|
||||||
e.type = eventTypeFromString(selectQuery.value(3).toString());
|
e.bankName = selectQuery.value(3).toString();
|
||||||
|
e.type = eventTypeFromString(selectQuery.value(4).toString());
|
||||||
cancelled.append(e);
|
cancelled.append(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -369,7 +373,7 @@ QList<EventInfo> EventDAO::cancelWaitingByBank(const QString &deviceId, const QS
|
|||||||
QList<EventInfo> cancelled;
|
QList<EventInfo> cancelled;
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
SELECT id, account_id, device_id, external_id, amount, phone, bank_name,
|
SELECT id, account_id, device_id, external_id, transaction_id, amount, phone, bank_name,
|
||||||
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
|
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
|
||||||
FROM events
|
FROM events
|
||||||
WHERE device_id = :device_id AND bank_name = :bank_name AND status = :wait
|
WHERE device_id = :device_id AND bank_name = :bank_name AND status = :wait
|
||||||
@ -425,7 +429,7 @@ QList<EventInfo> EventDAO::getRecentEvents(int hours) {
|
|||||||
QList<EventInfo> events;
|
QList<EventInfo> events;
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
SELECT id, account_id, device_id, external_id, amount, phone, bank_name,
|
SELECT id, account_id, device_id, external_id, transaction_id, amount, phone, bank_name,
|
||||||
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
|
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
|
||||||
FROM events
|
FROM events
|
||||||
WHERE timestamp >= datetime('now', :hours)
|
WHERE timestamp >= datetime('now', :hours)
|
||||||
|
|||||||
@ -91,6 +91,7 @@ CREATE TABLE IF NOT EXISTS events
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
account_id INTEGER,
|
account_id INTEGER,
|
||||||
external_id TEXT,
|
external_id TEXT,
|
||||||
|
transaction_id TEXT,
|
||||||
device_id TEXT,
|
device_id TEXT,
|
||||||
amount REAL,
|
amount REAL,
|
||||||
phone TEXT,
|
phone TEXT,
|
||||||
|
|||||||
@ -56,6 +56,7 @@ inline EventType eventTypeFromString(const QString &str) {
|
|||||||
struct EventInfo {
|
struct EventInfo {
|
||||||
int id = -1;
|
int id = -1;
|
||||||
QString externalId;
|
QString externalId;
|
||||||
|
QString transactionId;
|
||||||
int accountId = -1;
|
int accountId = -1;
|
||||||
QString deviceId;
|
QString deviceId;
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,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);
|
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data, int eventId = -1);
|
||||||
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description, const QByteArray &screenshot = {}, int eventId = -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 = {});
|
||||||
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) {}
|
||||||
@ -74,7 +74,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");
|
sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName, "App restarted, event was pending", {}, -1, e.transactionId);
|
||||||
qDebug() << "[EventHandler] Closed stale event id:" << e.id;
|
qDebug() << "[EventHandler] Closed stale event id:" << e.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,11 +108,14 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// transaction_id присутствует только в CREATE_TRANSACTION; для других типов это пусто.
|
||||||
|
const QString incomingTransactionId = task["body"].toObject()["transaction_id"].toString();
|
||||||
|
|
||||||
// Ищем аккаунт по material_id напрямую
|
// Ищем аккаунт по material_id напрямую
|
||||||
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);
|
sendTaskError(typeStr, materialId, bankName, "Account not found for material_id: " + materialId, {}, -1, incomingTransactionId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +123,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");
|
sendTaskError(eventTypeToString(c.type), c.externalId, c.bankName, "Cancelled: replaced by new task", {}, -1, c.transactionId);
|
||||||
EventDAO::markSentToServer(c.id);
|
EventDAO::markSentToServer(c.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,6 +145,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
|||||||
if (eventType == EventType::CreateTransaction) {
|
if (eventType == EventType::CreateTransaction) {
|
||||||
event.amount = body["amount"].toDouble() / 100.0;
|
event.amount = body["amount"].toDouble() / 100.0;
|
||||||
event.phone = body["bank_participator_id"].toString();
|
event.phone = body["bank_participator_id"].toString();
|
||||||
|
event.transactionId = body["transaction_id"].toString();
|
||||||
|
|
||||||
// Название банка-получателя из extra
|
// Название банка-получателя из extra
|
||||||
const QString bankNameRu = body["extra"].toObject()["bank_participator_name_ru"].toString();
|
const QString bankNameRu = body["extra"].toObject()["bank_participator_name_ru"].toString();
|
||||||
@ -160,7 +164,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");
|
sendTaskError(typeStr, materialId, bankName, "Device is offline", {}, -1, incomingTransactionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,11 +207,14 @@ 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) {
|
||||||
QJsonObject errorData;
|
QJsonObject errorData;
|
||||||
errorData["status"] = QStringLiteral("error");
|
errorData["status"] = QStringLiteral("error");
|
||||||
errorData["event_type"] = type;
|
errorData["event_type"] = type;
|
||||||
errorData["description"] = description;
|
errorData["description"] = description;
|
||||||
|
if (!transactionId.isEmpty())
|
||||||
|
errorData["transaction_id"] = transactionId;
|
||||||
sendTaskResult(QStringLiteral("ERROR"), materialId, bankName, errorData, eventId);
|
sendTaskResult(QStringLiteral("ERROR"), materialId, bankName, errorData, eventId);
|
||||||
|
|
||||||
if (!screenshot.isEmpty()) {
|
if (!screenshot.isEmpty()) {
|
||||||
@ -289,7 +296,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);
|
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result, screenshot, event.id, event.transactionId);
|
||||||
|
|
||||||
if (event.type == EventType::CreateTransaction) {
|
if (event.type == EventType::CreateTransaction) {
|
||||||
const MaterialInfo declAcc = MaterialDAO::getAccountById(event.accountId);
|
const MaterialInfo declAcc = MaterialDAO::getAccountById(event.accountId);
|
||||||
@ -318,6 +325,8 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject declined;
|
QJsonObject declined;
|
||||||
|
if (!event.transactionId.isEmpty())
|
||||||
|
declined["transaction_id"] = event.transactionId;
|
||||||
declined["bank_transaction_id"] = declTxId;
|
declined["bank_transaction_id"] = declTxId;
|
||||||
declined["amount"] = -static_cast<int>(event.amount * 100);
|
declined["amount"] = -static_cast<int>(event.amount * 100);
|
||||||
declined["currency"] = declCurrencyCode;
|
declined["currency"] = declCurrencyCode;
|
||||||
@ -360,7 +369,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
if (parsedTx.isEmpty()) {
|
if (parsedTx.isEmpty()) {
|
||||||
qWarning() << "[EventHandler] FETCH_TRANSACTIONS: script did not emit parsed tx";
|
qWarning() << "[EventHandler] FETCH_TRANSACTIONS: script did not emit parsed tx";
|
||||||
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName,
|
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName,
|
||||||
"Transaction not parsed from screen");
|
"Transaction not parsed from screen", {}, -1, event.transactionId);
|
||||||
EventDAO::updateEvent(event.id, EventStatus::Error,
|
EventDAO::updateEvent(event.id, EventStatus::Error,
|
||||||
"Transaction not parsed from screen");
|
"Transaction not parsed from screen");
|
||||||
EventDAO::markSentToServer(event.id);
|
EventDAO::markSentToServer(event.id);
|
||||||
@ -376,6 +385,8 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
const QString txType = taskJson["body"].toObject()["type"].toString();
|
const QString txType = taskJson["body"].toObject()["type"].toString();
|
||||||
|
|
||||||
QJsonObject obj;
|
QJsonObject obj;
|
||||||
|
if (!event.transactionId.isEmpty())
|
||||||
|
obj["transaction_id"] = event.transactionId;
|
||||||
obj["amount"] = -static_cast<int>(event.amount * 100);
|
obj["amount"] = -static_cast<int>(event.amount * 100);
|
||||||
obj["currency"] = currencyCode;
|
obj["currency"] = currencyCode;
|
||||||
obj["transaction_type"] = txType.isEmpty() ? "top-up" : txType;
|
obj["transaction_type"] = txType.isEmpty() ? "top-up" : txType;
|
||||||
@ -653,7 +664,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)");
|
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Script timeout (5 min)", {}, -1, event.transactionId);
|
||||||
EventDAO::markSentToServer(event.id);
|
EventDAO::markSentToServer(event.id);
|
||||||
|
|
||||||
// Убиваем банковское приложение на устройстве после таймаута
|
// Убиваем банковское приложение на устройстве после таймаута
|
||||||
@ -690,7 +701,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");
|
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Task expired: not started within 5 minutes", {}, -1, event.transactionId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -700,7 +711,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);
|
"Bank profile is disabled: " + account.appCode, {}, -1, event.transactionId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,7 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) {
|
|||||||
m_monitoringToken = settings.value("net/monitoringToken").toString();
|
m_monitoringToken = settings.value("net/monitoringToken").toString();
|
||||||
m_pathCurrentProfile = settings.value("net/pathCurrentProfile", "/api/v1/profile/current_profile").toString();
|
m_pathCurrentProfile = settings.value("net/pathCurrentProfile", "/api/v1/profile/current_profile").toString();
|
||||||
m_pathProfileGetInfo = settings.value("net/pathProfileGetInfo", "/api/v1/profile/get_info").toString();
|
m_pathProfileGetInfo = settings.value("net/pathProfileGetInfo", "/api/v1/profile/get_info").toString();
|
||||||
|
m_pathProfilePing = settings.value("net/pathProfilePing", "/api/v1/profile/ping").toString();
|
||||||
m_manager = new QNetworkAccessManager(this);
|
m_manager = new QNetworkAccessManager(this);
|
||||||
m_token = settings.value("common/token").toString();
|
m_token = settings.value("common/token").toString();
|
||||||
m_accessToken = SettingsDAO::get("access_token");
|
m_accessToken = SettingsDAO::get("access_token");
|
||||||
@ -279,7 +280,14 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
|
|||||||
}
|
}
|
||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
|
m_lastErrorSummary.clear();
|
||||||
|
m_lastResponseBody.clear();
|
||||||
|
m_lastHttpStatus = timedOut
|
||||||
|
? 0
|
||||||
|
: reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
if (timedOut) {
|
if (timedOut) {
|
||||||
|
m_lastErrorSummary = "Timeout after " + QString::number(kMaxRequestAttempts)
|
||||||
|
+ " attempts: " + lastTimeoutDetail;
|
||||||
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
||||||
"Timeout (POST) after " + QString::number(kMaxRequestAttempts)
|
"Timeout (POST) after " + QString::number(kMaxRequestAttempts)
|
||||||
+ " attempts: " + lastTimeoutDetail, {}, reqBodyStr, {});
|
+ " attempts: " + lastTimeoutDetail, {}, reqBodyStr, {});
|
||||||
@ -287,6 +295,8 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
|
|||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
const QByteArray responseBody = reply->readAll();
|
const QByteArray responseBody = reply->readAll();
|
||||||
const QString errSummary = describeNetError(reply, path);
|
const QString errSummary = describeNetError(reply, path);
|
||||||
|
m_lastErrorSummary = errSummary;
|
||||||
|
m_lastResponseBody = QString::fromUtf8(responseBody);
|
||||||
qCritical().noquote() << "Network error (POST):" << errSummary
|
qCritical().noquote() << "Network error (POST):" << errSummary
|
||||||
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
|
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
|
||||||
<< "\n response body:" << responseBody;
|
<< "\n response body:" << responseBody;
|
||||||
@ -303,6 +313,8 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
|
|||||||
if (root["success"].toBool()) {
|
if (root["success"].toBool()) {
|
||||||
result = root["data"];
|
result = root["data"];
|
||||||
} else {
|
} else {
|
||||||
|
m_lastErrorSummary = "API error (success=false)";
|
||||||
|
m_lastResponseBody = QString::fromUtf8(raw);
|
||||||
qCritical() << "API error:" << root;
|
qCritical() << "API error:" << root;
|
||||||
GeneralLogDAO::insert("WARNING", "NetworkService",
|
GeneralLogDAO::insert("WARNING", "NetworkService",
|
||||||
"API error (POST): " + shortPath, {},
|
"API error (POST): " + shortPath, {},
|
||||||
@ -1034,6 +1046,38 @@ bool NetworkService::syncCurrentProfile() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NetworkService::doPingProfile() {
|
||||||
|
if (!ensureValidToken()) return false;
|
||||||
|
|
||||||
|
const QString desktopId = SettingsDAO::get("desktop_id");
|
||||||
|
if (desktopId.isEmpty()) {
|
||||||
|
qWarning() << "[NetworkService] pingProfile: desktop_id is empty, skip";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QSettings settings("config.ini", QSettings::IniFormat);
|
||||||
|
QJsonObject body;
|
||||||
|
body["desktop_id"] = desktopId;
|
||||||
|
body["version"] = settings.value("common/version").toString();
|
||||||
|
|
||||||
|
const QJsonValue result = postJson(m_apiBase + m_pathProfilePing, body, true);
|
||||||
|
Q_UNUSED(result);
|
||||||
|
// Критерий успеха для пинга — HTTP 200, тело не важно.
|
||||||
|
if (m_lastHttpStatus != 200) {
|
||||||
|
qWarning().noquote() << "[NetworkService] pingProfile failed: HTTP"
|
||||||
|
<< m_lastHttpStatus
|
||||||
|
<< m_lastErrorSummary
|
||||||
|
<< "| response body:" << m_lastResponseBody;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkService::pingProfile() {
|
||||||
|
doPingProfile();
|
||||||
|
emit finished();
|
||||||
|
}
|
||||||
|
|
||||||
void NetworkService::postBankProfile() {
|
void NetworkService::postBankProfile() {
|
||||||
if (!ensureValidToken()) { emit finished(); return; }
|
if (!ensureValidToken()) { emit finished(); return; }
|
||||||
|
|
||||||
@ -1619,6 +1663,10 @@ void NetworkService::start() {
|
|||||||
// быть вызван явно после firstPassDone от DeviceScreener (см. правило #6
|
// быть вызван явно после firstPassDone от DeviceScreener (см. правило #6
|
||||||
// в docs/status-sync.md). Без firstPassDone мы не знаем, какие устройства
|
// в docs/status-sync.md). Без firstPassDone мы не знаем, какие устройства
|
||||||
// действительно онлайн, и можем ошибочно сработать как «device→offline».
|
// действительно онлайн, и можем ошибочно сработать как «device→offline».
|
||||||
|
|
||||||
|
// Heartbeat не зависит от firstPassDone — стартуем сразу. Если desktop_id
|
||||||
|
// ещё не получен (до loginAndSetup), первые тики просто скипнутся.
|
||||||
|
startPeriodicPing();
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::startPeriodicSync() {
|
void NetworkService::startPeriodicSync() {
|
||||||
@ -1628,6 +1676,33 @@ void NetworkService::startPeriodicSync() {
|
|||||||
scheduleNextStatusSync();
|
scheduleNextStatusSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkService::startPeriodicPing() {
|
||||||
|
if (m_pingEnabled) return;
|
||||||
|
m_pingEnabled = true;
|
||||||
|
qDebug() << "[NetworkService] periodic ping enabled (every" << (kPingProfileMs / 1000) << "s)";
|
||||||
|
scheduleNextPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkService::scheduleNextPing() {
|
||||||
|
if (!m_running || !m_pingEnabled) return;
|
||||||
|
|
||||||
|
QTimer::singleShot(kPingProfileMs, this, [this]() {
|
||||||
|
if (!m_running) return;
|
||||||
|
|
||||||
|
// Если предыдущий ping ещё в пути — пропускаем тик, чтобы не наслаивать.
|
||||||
|
if (m_pingInProgress.exchange(true)) {
|
||||||
|
qDebug() << "[NetworkService] ping tick skipped — previous still in progress";
|
||||||
|
scheduleNextPing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doPingProfile();
|
||||||
|
m_pingInProgress.store(false);
|
||||||
|
|
||||||
|
scheduleNextPing();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void NetworkService::scheduleNextStatusSync() {
|
void NetworkService::scheduleNextStatusSync() {
|
||||||
if (!m_running || !m_periodicSyncEnabled) return;
|
if (!m_running || !m_periodicSyncEnabled) return;
|
||||||
|
|
||||||
|
|||||||
@ -63,6 +63,13 @@ public slots:
|
|||||||
// health-счётчиком в runPeriodicSyncTick (правило #9).
|
// health-счётчиком в runPeriodicSyncTick (правило #9).
|
||||||
bool syncCurrentProfile();
|
bool syncCurrentProfile();
|
||||||
|
|
||||||
|
// POST /api/v1/profile/ping — heartbeat: даёт серверу понять, что десктоп
|
||||||
|
// онлайн. В теле уходят desktop_id и версия клиента. Вызывать периодически.
|
||||||
|
void pingProfile();
|
||||||
|
|
||||||
|
// Включает периодический ping (kPingProfileMs). Идемпотентен.
|
||||||
|
void startPeriodicPing();
|
||||||
|
|
||||||
// Включает периодическую синхронизацию статусов (60s). Должно быть вызвано
|
// Включает периодическую синхронизацию статусов (60s). Должно быть вызвано
|
||||||
// ОДИН раз для долгоживущего инстанса после firstPassDone от DeviceScreener.
|
// ОДИН раз для долгоживущего инстанса после firstPassDone от DeviceScreener.
|
||||||
void startPeriodicSync();
|
void startPeriodicSync();
|
||||||
@ -142,8 +149,15 @@ private:
|
|||||||
QString m_monitoringToken;
|
QString m_monitoringToken;
|
||||||
QString m_pathCurrentProfile;
|
QString m_pathCurrentProfile;
|
||||||
QString m_pathProfileGetInfo;
|
QString m_pathProfileGetInfo;
|
||||||
|
QString m_pathProfilePing;
|
||||||
bool m_fetchingTasks = false;
|
bool m_fetchingTasks = false;
|
||||||
|
|
||||||
|
// Последняя ошибка от postJson/patchJson/getJson — чтобы вызывающий код
|
||||||
|
// мог логировать тело ответа сервера, не дублируя парсинг ошибки.
|
||||||
|
QString m_lastErrorSummary;
|
||||||
|
QString m_lastResponseBody;
|
||||||
|
int m_lastHttpStatus = 0; // 0 если ответа не было (timeout); иначе HTTP-код
|
||||||
|
|
||||||
// Периодическая синхронизация
|
// Периодическая синхронизация
|
||||||
bool m_periodicSyncEnabled = false; // включается только после firstPassDone
|
bool m_periodicSyncEnabled = false; // включается только после firstPassDone
|
||||||
std::atomic<bool> m_syncInProgress{false}; // защита от наложения тиков
|
std::atomic<bool> m_syncInProgress{false}; // защита от наложения тиков
|
||||||
@ -151,6 +165,11 @@ private:
|
|||||||
bool m_unhealthyEmitted = false; // чтобы не спамить syncHealthChanged
|
bool m_unhealthyEmitted = false; // чтобы не спамить syncHealthChanged
|
||||||
static constexpr int kSyncFailureThreshold = 3;
|
static constexpr int kSyncFailureThreshold = 3;
|
||||||
static constexpr int kPeriodicSyncMs = 60 * 1000;
|
static constexpr int kPeriodicSyncMs = 60 * 1000;
|
||||||
|
static constexpr int kPingProfileMs = 5 * 1000;
|
||||||
|
|
||||||
|
// Heartbeat (ping)
|
||||||
|
bool m_pingEnabled = false;
|
||||||
|
std::atomic<bool> m_pingInProgress{false};
|
||||||
|
|
||||||
// Реестр мьютексов на пару (deviceId, bankProfileId) — общий для всех
|
// Реестр мьютексов на пару (deviceId, bankProfileId) — общий для всех
|
||||||
// инстансов NetworkService. Используется для сериализации PATCH-операций.
|
// инстансов NetworkService. Используется для сериализации PATCH-операций.
|
||||||
@ -171,6 +190,8 @@ private:
|
|||||||
void scheduleNextRefresh();
|
void scheduleNextRefresh();
|
||||||
void scheduleNextStatusSync();
|
void scheduleNextStatusSync();
|
||||||
bool runPeriodicSyncTick(); // делает один тик, возвращает true при успехе
|
bool runPeriodicSyncTick(); // делает один тик, возвращает true при успехе
|
||||||
|
bool doPingProfile(); // тело ping без emit finished — для периодических тиков
|
||||||
|
void scheduleNextPing();
|
||||||
void syncDevicesFromServer(const QString &desktopId);
|
void syncDevicesFromServer(const QString &desktopId);
|
||||||
void syncBankProfilesFromServer();
|
void syncBankProfilesFromServer();
|
||||||
void syncBanksIfEmpty();
|
void syncBanksIfEmpty();
|
||||||
|
|||||||
@ -247,11 +247,17 @@ void AccountSettingsWindow::toggleProfileStatus() {
|
|||||||
|
|
||||||
m_app.status = newStatus;
|
m_app.status = newStatus;
|
||||||
showOverlay(isActive ? "Выключение профиля..." : "Включение профиля...");
|
showOverlay(isActive ? "Выключение профиля..." : "Включение профиля...");
|
||||||
sendAppStatus(newStatus);
|
|
||||||
|
|
||||||
// При включении профиля — включаем на сервере все локально активные материалы
|
// При включении профиля — материалы включаем ТОЛЬКО после успешного
|
||||||
|
// включения bank_profile на сервере; иначе пользователь получит подряд
|
||||||
|
// несколько одинаковых ошибок ("Invalid or disabled connection") от
|
||||||
|
// независимых PATCH'ей.
|
||||||
|
std::function<void()> onSuccess;
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_app.code);
|
const QString deviceId = m_deviceId;
|
||||||
|
const QString appCode = m_app.code;
|
||||||
|
onSuccess = [this, deviceId, appCode]() {
|
||||||
|
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(deviceId, appCode);
|
||||||
for (const auto &mat : materials) {
|
for (const auto &mat : materials) {
|
||||||
if (mat.status == MaterialStatus::Active && !mat.materialId.isEmpty()) {
|
if (mat.status == MaterialStatus::Active && !mat.materialId.isEmpty()) {
|
||||||
auto *thread = new QThread;
|
auto *thread = new QThread;
|
||||||
@ -268,7 +274,9 @@ void AccountSettingsWindow::toggleProfileStatus() {
|
|||||||
qDebug() << "[AccountSettings] Enabling material on server:" << mat.materialId;
|
qDebug() << "[AccountSettings] Enabling material on server:" << mat.materialId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
sendAppStatus(newStatus, std::move(onSuccess));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Edit PIN ───────────────────────────────────────────────────────────────
|
// ── Edit PIN ───────────────────────────────────────────────────────────────
|
||||||
@ -463,7 +471,7 @@ void AccountSettingsWindow::startGetCardInfo() {
|
|||||||
|
|
||||||
// ── Send status to server ──────────────────────────────────────────────────
|
// ── Send status to server ──────────────────────────────────────────────────
|
||||||
|
|
||||||
void AccountSettingsWindow::sendAppStatus(const QString &status) {
|
void AccountSettingsWindow::sendAppStatus(const QString &status, std::function<void()> onSuccess) {
|
||||||
if (m_app.bankProfileId.isEmpty()) {
|
if (m_app.bankProfileId.isEmpty()) {
|
||||||
qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << m_app.code;
|
qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << m_app.code;
|
||||||
hideOverlay(true, "Сохранено локально", [this]() { updateStatusUI(); });
|
hideOverlay(true, "Сохранено локально", [this]() { updateStatusUI(); });
|
||||||
@ -478,9 +486,10 @@ void AccountSettingsWindow::sendAppStatus(const QString &status) {
|
|||||||
net->addExtra("enable", enable);
|
net->addExtra("enable", enable);
|
||||||
NetworkService::connectErrorDialog(this, net);
|
NetworkService::connectErrorDialog(this, net);
|
||||||
connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
||||||
connect(net, &NetworkService::finishedWithResult, this, [this, enable](int result) {
|
connect(net, &NetworkService::finishedWithResult, this, [this, enable, onSuccess = std::move(onSuccess)](int result) {
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
hideOverlay(true, "Успешно", [this]() { updateStatusUI(); });
|
hideOverlay(true, "Успешно", [this]() { updateStatusUI(); });
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Правило #3: при неудачном включении — откатываем локаль в off и
|
// Правило #3: при неудачном включении — откатываем локаль в off и
|
||||||
|
|||||||
@ -44,7 +44,7 @@ private:
|
|||||||
void startCheckPinCode();
|
void startCheckPinCode();
|
||||||
void startGetProfileInfo();
|
void startGetProfileInfo();
|
||||||
void startGetCardInfo();
|
void startGetCardInfo();
|
||||||
void sendAppStatus(const QString &status);
|
void sendAppStatus(const QString &status, std::function<void()> onSuccess = {});
|
||||||
void toggleMaterialStatus(const MaterialInfo &account);
|
void toggleMaterialStatus(const MaterialInfo &account);
|
||||||
void reloadCards();
|
void reloadCards();
|
||||||
|
|
||||||
|
|||||||
@ -92,11 +92,14 @@ void BankSettingsWindow::startCheckPinCode(const QString &appCode) {
|
|||||||
thread->start();
|
thread->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
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 &transactionId = {}) {
|
||||||
QJsonObject errData;
|
QJsonObject errData;
|
||||||
errData["status"] = "error";
|
errData["status"] = "error";
|
||||||
errData["event_type"] = type;
|
errData["event_type"] = type;
|
||||||
errData["description"] = "Bank profile disabled";
|
errData["description"] = "Bank profile disabled";
|
||||||
|
if (!transactionId.isEmpty())
|
||||||
|
errData["transaction_id"] = transactionId;
|
||||||
|
|
||||||
QJsonObject obj;
|
QJsonObject obj;
|
||||||
obj["type"] = "ERROR";
|
obj["type"] = "ERROR";
|
||||||
@ -126,7 +129,7 @@ void sendAppStatus(const QString &deviceId, const QString &code, const QString &
|
|||||||
if (status != "active") {
|
if (status != "active") {
|
||||||
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByBank(deviceId, code);
|
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByBank(deviceId, code);
|
||||||
for (const EventInfo &e : cancelled) {
|
for (const EventInfo &e : cancelled) {
|
||||||
sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName);
|
sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName, e.transactionId);
|
||||||
EventDAO::markSentToServer(e.id);
|
EventDAO::markSentToServer(e.id);
|
||||||
qDebug() << "[BankSettings] cancelled task" << e.id << eventTypeToString(e.type);
|
qDebug() << "[BankSettings] cancelled task" << e.id << eventTypeToString(e.type);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,11 +58,14 @@ QString getColorByStatus(const BankProfileInfo &app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void sendTaskErrorToServer(const QString &type, const QString &materialId, const QString &bankName) {
|
static void sendTaskErrorToServer(const QString &type, const QString &materialId, const QString &bankName,
|
||||||
|
const QString &transactionId = {}) {
|
||||||
QJsonObject errData;
|
QJsonObject errData;
|
||||||
errData["status"] = "error";
|
errData["status"] = "error";
|
||||||
errData["event_type"] = type;
|
errData["event_type"] = type;
|
||||||
errData["description"] = "Bank profile disabled";
|
errData["description"] = "Bank profile disabled";
|
||||||
|
if (!transactionId.isEmpty())
|
||||||
|
errData["transaction_id"] = transactionId;
|
||||||
|
|
||||||
QJsonObject obj;
|
QJsonObject obj;
|
||||||
obj["type"] = "ERROR";
|
obj["type"] = "ERROR";
|
||||||
@ -94,7 +97,7 @@ void sendBankStatus(const QString &deviceId, const QString &code, const QString
|
|||||||
if (status != "active") {
|
if (status != "active") {
|
||||||
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByBank(deviceId, code);
|
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByBank(deviceId, code);
|
||||||
for (const EventInfo &e : cancelled) {
|
for (const EventInfo &e : cancelled) {
|
||||||
sendTaskErrorToServer(eventTypeToString(e.type), e.externalId, e.bankName);
|
sendTaskErrorToServer(eventTypeToString(e.type), e.externalId, e.bankName, e.transactionId);
|
||||||
EventDAO::markSentToServer(e.id);
|
EventDAO::markSentToServer(e.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +110,9 @@ void sendBankStatus(const QString &deviceId, const QString &code, const QString
|
|||||||
net->addExtra("enable", enable);
|
net->addExtra("enable", enable);
|
||||||
QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
||||||
if (onResult) {
|
if (onResult) {
|
||||||
QObject::connect(net, &NetworkService::finishedWithResult, net,
|
// Контекст — qApp (main thread), иначе onResult выполнится в worker-треде
|
||||||
|
// и QMessageBox внутри колбэка упадёт на NSAlert init (см. crashlog 2026-05-26).
|
||||||
|
QObject::connect(net, &NetworkService::finishedWithResult, qApp,
|
||||||
[onResult](int result) { onResult(result); },
|
[onResult](int result) { onResult(result); },
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
@ -368,44 +373,44 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
|
|||||||
window->activateWindow();
|
window->activateWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
auto *deleteBtn = new QPushButton(this);
|
// auto *deleteBtn = new QPushButton(this);
|
||||||
deleteBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/delete.png"));
|
// deleteBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/delete.png"));
|
||||||
deleteBtn->setIconSize(QSize(20, 20));
|
// deleteBtn->setIconSize(QSize(20, 20));
|
||||||
deleteBtn->setFixedSize(28, 28);
|
// deleteBtn->setFixedSize(28, 28);
|
||||||
deleteBtn->setFlat(true);
|
// deleteBtn->setFlat(true);
|
||||||
deleteBtn->setStyleSheet(
|
// deleteBtn->setStyleSheet(
|
||||||
"background-color: rgba(0, 0, 0, 0.5);"
|
// "background-color: rgba(0, 0, 0, 0.5);"
|
||||||
"border: none;"
|
// "border: none;"
|
||||||
"border-radius: 4px;"
|
// "border-radius: 4px;"
|
||||||
);
|
// );
|
||||||
connect(deleteBtn, &QPushButton::clicked, this, [this]() {
|
// connect(deleteBtn, &QPushButton::clicked, this, [this]() {
|
||||||
const QString deviceId = m_device.id;
|
// const QString deviceId = m_device.id;
|
||||||
const QString deviceName = m_device.name;
|
// const QString deviceName = m_device.name;
|
||||||
QPointer<DeviceWidget> guard(this);
|
// QPointer<DeviceWidget> guard(this);
|
||||||
|
//
|
||||||
QMessageBox msgBox(nullptr);
|
// QMessageBox msgBox(nullptr);
|
||||||
msgBox.setWindowTitle(tr("Удалить устройство?"));
|
// msgBox.setWindowTitle(tr("Удалить устройство?"));
|
||||||
msgBox.setText(
|
// msgBox.setText(
|
||||||
"Будет удалено устройство " + deviceName +
|
// "Будет удалено устройство " + deviceName +
|
||||||
" и все связанные данные: банки, аккаунты, транзакции и события.");
|
// " и все связанные данные: банки, аккаунты, транзакции и события.");
|
||||||
QPushButton *confirmBtn = msgBox.addButton(tr("Удалить"), QMessageBox::AcceptRole);
|
// QPushButton *confirmBtn = msgBox.addButton(tr("Удалить"), QMessageBox::AcceptRole);
|
||||||
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
|
// QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
|
||||||
msgBox.setDefaultButton(cancelBtn);
|
// msgBox.setDefaultButton(cancelBtn);
|
||||||
msgBox.setModal(true);
|
// msgBox.setModal(true);
|
||||||
msgBox.exec();
|
// msgBox.exec();
|
||||||
|
//
|
||||||
if (msgBox.clickedButton() == confirmBtn) {
|
// if (msgBox.clickedButton() == confirmBtn) {
|
||||||
auto *thread = new QThread;
|
// auto *thread = new QThread;
|
||||||
auto *net = new NetworkService;
|
// auto *net = new NetworkService;
|
||||||
net->setDeviceId(deviceId);
|
// net->setDeviceId(deviceId);
|
||||||
net->moveToThread(thread);
|
// net->moveToThread(thread);
|
||||||
QObject::connect(thread, &QThread::started, net, &NetworkService::deleteDeviceFromServer);
|
// QObject::connect(thread, &QThread::started, net, &NetworkService::deleteDeviceFromServer);
|
||||||
QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
|
// QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||||||
QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
// QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||||||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
// QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
thread->start();
|
// thread->start();
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
auto *codeBtn = new QPushButton(this);
|
auto *codeBtn = new QPushButton(this);
|
||||||
codeBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/code.png"));
|
codeBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/code.png"));
|
||||||
@ -620,7 +625,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
|
|||||||
});
|
});
|
||||||
|
|
||||||
topRightLayout->addWidget(settingsBtn);
|
topRightLayout->addWidget(settingsBtn);
|
||||||
topRightLayout->addWidget(deleteBtn);
|
// topRightLayout->addWidget(deleteBtn);
|
||||||
topRightLayout->addWidget(codeBtn);
|
topRightLayout->addWidget(codeBtn);
|
||||||
layout->addWidget(topRightWidget, 0, 0, Qt::AlignTop | Qt::AlignRight);
|
layout->addWidget(topRightWidget, 0, 0, Qt::AlignTop | Qt::AlignRight);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user