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.
(cherry picked from commit 067b16f700)
This commit is contained in:
parent
d3b109d87f
commit
50a7b20f0a
@ -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
|
||||||
|
|||||||
@ -173,25 +173,27 @@ void DatabaseManager::initSchema() {
|
|||||||
))");
|
))");
|
||||||
|
|
||||||
q.exec(R"(CREATE TABLE IF NOT EXISTS events (
|
q.exec(R"(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,
|
||||||
device_id TEXT,
|
transaction_id TEXT,
|
||||||
amount REAL,
|
device_id TEXT,
|
||||||
phone TEXT,
|
amount REAL,
|
||||||
bank_name TEXT,
|
phone TEXT,
|
||||||
comment TEXT,
|
bank_name TEXT,
|
||||||
status TEXT,
|
comment TEXT,
|
||||||
type TEXT,
|
status TEXT,
|
||||||
update_time DATETIME,
|
type TEXT,
|
||||||
complete_time DATETIME,
|
update_time DATETIME,
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
complete_time DATETIME,
|
||||||
json TEXT DEFAULT '',
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
json TEXT DEFAULT '',
|
||||||
sent_to_server INTEGER DEFAULT 0,
|
sent_to_server INTEGER DEFAULT 0,
|
||||||
screenshot BLOB,
|
screenshot BLOB,
|
||||||
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)
|
||||||
|
|||||||
@ -88,22 +88,23 @@ CREATE TABLE IF NOT EXISTS transactions
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS events
|
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,
|
||||||
device_id TEXT,
|
transaction_id TEXT,
|
||||||
amount REAL,
|
device_id TEXT,
|
||||||
phone TEXT,
|
amount REAL,
|
||||||
bank_name TEXT,
|
phone TEXT,
|
||||||
comment TEXT,
|
bank_name TEXT,
|
||||||
status TEXT,
|
comment TEXT,
|
||||||
type TEXT,
|
status TEXT,
|
||||||
update_time DATETIME,
|
type TEXT,
|
||||||
complete_time DATETIME,
|
update_time DATETIME,
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
complete_time DATETIME,
|
||||||
json TEXT DEFAULT '',
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
json TEXT DEFAULT '',
|
||||||
sent_to_server INTEGER DEFAULT 0,
|
sent_to_server INTEGER DEFAULT 0,
|
||||||
screenshot BLOB,
|
screenshot BLOB,
|
||||||
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
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,28 +247,36 @@ 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;
|
||||||
for (const auto &mat : materials) {
|
const QString appCode = m_app.code;
|
||||||
if (mat.status == MaterialStatus::Active && !mat.materialId.isEmpty()) {
|
onSuccess = [this, deviceId, appCode]() {
|
||||||
auto *thread = new QThread;
|
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(deviceId, appCode);
|
||||||
auto *net = new NetworkService;
|
for (const auto &mat : materials) {
|
||||||
net->moveToThread(thread);
|
if (mat.status == MaterialStatus::Active && !mat.materialId.isEmpty()) {
|
||||||
net->addExtra("material_id", mat.materialId);
|
auto *thread = new QThread;
|
||||||
net->addExtra("enable", true);
|
auto *net = new NetworkService;
|
||||||
NetworkService::connectErrorDialog(this, net);
|
net->moveToThread(thread);
|
||||||
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
|
net->addExtra("material_id", mat.materialId);
|
||||||
connect(net, &NetworkService::finished, thread, &QThread::quit);
|
net->addExtra("enable", true);
|
||||||
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
NetworkService::connectErrorDialog(this, net);
|
||||||
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
|
||||||
thread->start();
|
connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||||||
qDebug() << "[AccountSettings] Enabling material on server:" << mat.materialId;
|
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||||||
|
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
thread->start();
|
||||||
|
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