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
d031eed7d2
commit
067b16f700
@ -18,6 +18,7 @@ pathDesktopCreate=/api/v1/desktop/
|
||||
pathDevice=/api/v1/device/
|
||||
pathDeviceScreenshot=/api/v1/device/add_screenshot/
|
||||
pathMaterial=/api/v1/profile/material
|
||||
pathProfilePing=/api/v1/profile/ping
|
||||
pathMonitoringLog=/monitoring/log
|
||||
pathMonitoringDump=/monitoring/dump
|
||||
monitoringBase=http://142.93.102.40:8080
|
||||
|
||||
@ -173,25 +173,27 @@ void DatabaseManager::initSchema() {
|
||||
))");
|
||||
|
||||
q.exec(R"(CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER,
|
||||
external_id TEXT,
|
||||
device_id TEXT,
|
||||
amount REAL,
|
||||
phone TEXT,
|
||||
bank_name TEXT,
|
||||
comment TEXT,
|
||||
status TEXT,
|
||||
type TEXT,
|
||||
update_time DATETIME,
|
||||
complete_time DATETIME,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
json TEXT DEFAULT '',
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER,
|
||||
external_id TEXT,
|
||||
transaction_id TEXT,
|
||||
device_id TEXT,
|
||||
amount REAL,
|
||||
phone TEXT,
|
||||
bank_name TEXT,
|
||||
comment TEXT,
|
||||
status TEXT,
|
||||
type TEXT,
|
||||
update_time DATETIME,
|
||||
complete_time DATETIME,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
json TEXT DEFAULT '',
|
||||
sent_to_server INTEGER DEFAULT 0,
|
||||
screenshot BLOB,
|
||||
screenshot BLOB,
|
||||
FOREIGN KEY (account_id) REFERENCES materials (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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
@ -13,6 +13,7 @@ EventInfo parseEvent(const QSqlQuery &query) {
|
||||
event.status = eventStatusFromString(query.value("status").toString());
|
||||
event.type = eventTypeFromString(query.value("type").toString());
|
||||
event.externalId = query.value("external_id").toString();
|
||||
event.transactionId = query.value("transaction_id").toString();
|
||||
event.accountId = query.value("account_id").toInt();
|
||||
event.deviceId = query.value("device_id").toString();
|
||||
event.amount = query.value("amount").toDouble();
|
||||
@ -36,15 +37,16 @@ int EventDAO::insertEvent(const EventInfo &event) {
|
||||
|
||||
query.prepare(R"(
|
||||
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 (
|
||||
: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(":type", eventTypeToString(event.type));
|
||||
query.bindValue(":external_id", event.externalId);
|
||||
query.bindValue(":transaction_id", event.transactionId);
|
||||
query.bindValue(":account_id", event.accountId);
|
||||
query.bindValue(":device_id", event.deviceId);
|
||||
query.bindValue(":amount", event.amount);
|
||||
@ -139,7 +141,7 @@ QList<EventInfo> EventDAO::getAllEvents(const EventStatus &status) {
|
||||
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
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
|
||||
FROM events
|
||||
WHERE status = :status
|
||||
@ -161,7 +163,7 @@ QList<EventInfo> EventDAO::getAllEvents(const EventStatus &status) {
|
||||
EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) {
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
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
|
||||
FROM events
|
||||
WHERE status = :status AND device_id = :device_id
|
||||
@ -193,7 +195,7 @@ QList<EventInfo> EventDAO::getEvents(int page, int pageSize) {
|
||||
QList<EventInfo> events;
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
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,
|
||||
(LENGTH(screenshot) > 0) as has_screenshot
|
||||
FROM events
|
||||
@ -276,7 +278,7 @@ QList<EventInfo> EventDAO::cancelWaitingByMaterial(const QString &materialId, Ev
|
||||
QList<EventInfo> cancelled;
|
||||
QSqlQuery selectQuery(DatabaseManager::instance().database());
|
||||
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
|
||||
)");
|
||||
selectQuery.bindValue(":material_id", materialId);
|
||||
@ -288,8 +290,9 @@ QList<EventInfo> EventDAO::cancelWaitingByMaterial(const QString &materialId, Ev
|
||||
EventInfo e;
|
||||
e.id = selectQuery.value(0).toInt();
|
||||
e.externalId = selectQuery.value(1).toString();
|
||||
e.bankName = selectQuery.value(2).toString();
|
||||
e.type = eventTypeFromString(selectQuery.value(3).toString());
|
||||
e.transactionId = selectQuery.value(2).toString();
|
||||
e.bankName = selectQuery.value(3).toString();
|
||||
e.type = eventTypeFromString(selectQuery.value(4).toString());
|
||||
cancelled.append(e);
|
||||
}
|
||||
}
|
||||
@ -315,12 +318,12 @@ QList<EventInfo> EventDAO::cancelWaitingByType(const QString &deviceId, EventTyp
|
||||
QSqlQuery selectQuery(DatabaseManager::instance().database());
|
||||
if (bankName.isEmpty()) {
|
||||
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
|
||||
)");
|
||||
} else {
|
||||
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
|
||||
)");
|
||||
selectQuery.bindValue(":bank_name", bankName);
|
||||
@ -334,8 +337,9 @@ QList<EventInfo> EventDAO::cancelWaitingByType(const QString &deviceId, EventTyp
|
||||
EventInfo e;
|
||||
e.id = selectQuery.value(0).toInt();
|
||||
e.externalId = selectQuery.value(1).toString();
|
||||
e.bankName = selectQuery.value(2).toString();
|
||||
e.type = eventTypeFromString(selectQuery.value(3).toString());
|
||||
e.transactionId = selectQuery.value(2).toString();
|
||||
e.bankName = selectQuery.value(3).toString();
|
||||
e.type = eventTypeFromString(selectQuery.value(4).toString());
|
||||
cancelled.append(e);
|
||||
}
|
||||
}
|
||||
@ -369,7 +373,7 @@ QList<EventInfo> EventDAO::cancelWaitingByBank(const QString &deviceId, const QS
|
||||
QList<EventInfo> cancelled;
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
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
|
||||
FROM events
|
||||
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;
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
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
|
||||
FROM events
|
||||
WHERE timestamp >= datetime('now', :hours)
|
||||
|
||||
@ -88,22 +88,23 @@ CREATE TABLE IF NOT EXISTS transactions
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER,
|
||||
external_id TEXT,
|
||||
device_id TEXT,
|
||||
amount REAL,
|
||||
phone TEXT,
|
||||
bank_name TEXT,
|
||||
comment TEXT,
|
||||
status TEXT,
|
||||
type TEXT,
|
||||
update_time DATETIME,
|
||||
complete_time DATETIME,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
json TEXT DEFAULT '',
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER,
|
||||
external_id TEXT,
|
||||
transaction_id TEXT,
|
||||
device_id TEXT,
|
||||
amount REAL,
|
||||
phone TEXT,
|
||||
bank_name TEXT,
|
||||
comment TEXT,
|
||||
status TEXT,
|
||||
type TEXT,
|
||||
update_time DATETIME,
|
||||
complete_time DATETIME,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
json TEXT DEFAULT '',
|
||||
sent_to_server INTEGER DEFAULT 0,
|
||||
screenshot BLOB,
|
||||
screenshot BLOB,
|
||||
FOREIGN KEY (account_id) REFERENCES materials (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 {
|
||||
int id = -1;
|
||||
QString externalId;
|
||||
QString transactionId;
|
||||
int accountId = -1;
|
||||
QString deviceId;
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
#include "dushanbe/PayByPhoneScript.h"
|
||||
|
||||
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);
|
||||
|
||||
EventHandler::EventHandler(QObject *parent) : QObject(parent) {}
|
||||
@ -74,7 +74,7 @@ void EventHandler::start() {
|
||||
if (e.sentToServer) continue;
|
||||
EventDAO::updateEvent(e.id, EventStatus::Error, "App restarted");
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -108,11 +108,14 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// transaction_id присутствует только в CREATE_TRANSACTION; для других типов это пусто.
|
||||
const QString incomingTransactionId = task["body"].toObject()["transaction_id"].toString();
|
||||
|
||||
// Ищем аккаунт по material_id напрямую
|
||||
const MaterialInfo account = MaterialDAO::findByMaterialId(materialId);
|
||||
if (account.id == -1) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -120,7 +123,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
||||
if (eventType == EventType::FetchProfile) {
|
||||
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByMaterial(materialId, EventType::FetchProfile);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -142,6 +145,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
||||
if (eventType == EventType::CreateTransaction) {
|
||||
event.amount = body["amount"].toDouble() / 100.0;
|
||||
event.phone = body["bank_participator_id"].toString();
|
||||
event.transactionId = body["transaction_id"].toString();
|
||||
|
||||
// Название банка-получателя из extra
|
||||
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)) {
|
||||
qWarning() << "[EventHandler] Device offline for material_id:" << materialId << "device:" << account.deviceId;
|
||||
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,
|
||||
const QString &description, const QByteArray &screenshot, int eventId) {
|
||||
const QString &description, const QByteArray &screenshot, int eventId,
|
||||
const QString &transactionId) {
|
||||
QJsonObject errorData;
|
||||
errorData["status"] = QStringLiteral("error");
|
||||
errorData["event_type"] = type;
|
||||
errorData["description"] = description;
|
||||
if (!transactionId.isEmpty())
|
||||
errorData["transaction_id"] = transactionId;
|
||||
sendTaskResult(QStringLiteral("ERROR"), materialId, bankName, errorData, eventId);
|
||||
|
||||
if (!screenshot.isEmpty()) {
|
||||
@ -289,7 +296,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
||||
.arg(eventTypeToString(event.type), deviceLabel, event.bankName, result);
|
||||
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) {
|
||||
const MaterialInfo declAcc = MaterialDAO::getAccountById(event.accountId);
|
||||
@ -318,6 +325,8 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
||||
}
|
||||
|
||||
QJsonObject declined;
|
||||
if (!event.transactionId.isEmpty())
|
||||
declined["transaction_id"] = event.transactionId;
|
||||
declined["bank_transaction_id"] = declTxId;
|
||||
declined["amount"] = -static_cast<int>(event.amount * 100);
|
||||
declined["currency"] = declCurrencyCode;
|
||||
@ -360,7 +369,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
||||
if (parsedTx.isEmpty()) {
|
||||
qWarning() << "[EventHandler] FETCH_TRANSACTIONS: script did not emit parsed tx";
|
||||
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,
|
||||
"Transaction not parsed from screen");
|
||||
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();
|
||||
|
||||
QJsonObject obj;
|
||||
if (!event.transactionId.isEmpty())
|
||||
obj["transaction_id"] = event.transactionId;
|
||||
obj["amount"] = -static_cast<int>(event.amount * 100);
|
||||
obj["currency"] = currencyCode;
|
||||
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")
|
||||
.arg(eventTypeToString(event.type), event.bankName, event.externalId);
|
||||
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);
|
||||
|
||||
// Убиваем банковское приложение на устройстве после таймаута
|
||||
@ -690,7 +701,7 @@ void EventHandler::processNextTask(const QString &deviceId) {
|
||||
if (waitSecs > 5 * 60) {
|
||||
qWarning() << "[EventHandler] Task expired after" << waitSecs << "s, event id:" << event.id;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -700,7 +711,7 @@ void EventHandler::processNextTask(const QString &deviceId) {
|
||||
qWarning() << "[EventHandler] Bank profile disabled for" << account.appCode << "device:" << deviceId;
|
||||
EventDAO::updateEvent(event.id, EventStatus::Error, "Bank profile is disabled");
|
||||
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName,
|
||||
"Bank profile is disabled: " + account.appCode);
|
||||
"Bank profile is disabled: " + account.appCode, {}, -1, event.transactionId);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@ -81,6 +81,7 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) {
|
||||
m_monitoringToken = settings.value("net/monitoringToken").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_pathProfilePing = settings.value("net/pathProfilePing", "/api/v1/profile/ping").toString();
|
||||
m_manager = new QNetworkAccessManager(this);
|
||||
m_token = settings.value("common/token").toString();
|
||||
m_accessToken = SettingsDAO::get("access_token");
|
||||
@ -279,7 +280,14 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
|
||||
}
|
||||
|
||||
QJsonValue result;
|
||||
m_lastErrorSummary.clear();
|
||||
m_lastResponseBody.clear();
|
||||
m_lastHttpStatus = timedOut
|
||||
? 0
|
||||
: reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (timedOut) {
|
||||
m_lastErrorSummary = "Timeout after " + QString::number(kMaxRequestAttempts)
|
||||
+ " attempts: " + lastTimeoutDetail;
|
||||
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
||||
"Timeout (POST) after " + QString::number(kMaxRequestAttempts)
|
||||
+ " attempts: " + lastTimeoutDetail, {}, reqBodyStr, {});
|
||||
@ -287,6 +295,8 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
const QByteArray responseBody = reply->readAll();
|
||||
const QString errSummary = describeNetError(reply, path);
|
||||
m_lastErrorSummary = errSummary;
|
||||
m_lastResponseBody = QString::fromUtf8(responseBody);
|
||||
qCritical().noquote() << "Network error (POST):" << errSummary
|
||||
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
|
||||
<< "\n response body:" << responseBody;
|
||||
@ -303,6 +313,8 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
|
||||
if (root["success"].toBool()) {
|
||||
result = root["data"];
|
||||
} else {
|
||||
m_lastErrorSummary = "API error (success=false)";
|
||||
m_lastResponseBody = QString::fromUtf8(raw);
|
||||
qCritical() << "API error:" << root;
|
||||
GeneralLogDAO::insert("WARNING", "NetworkService",
|
||||
"API error (POST): " + shortPath, {},
|
||||
@ -1034,6 +1046,38 @@ bool NetworkService::syncCurrentProfile() {
|
||||
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() {
|
||||
if (!ensureValidToken()) { emit finished(); return; }
|
||||
|
||||
@ -1619,6 +1663,10 @@ void NetworkService::start() {
|
||||
// быть вызван явно после firstPassDone от DeviceScreener (см. правило #6
|
||||
// в docs/status-sync.md). Без firstPassDone мы не знаем, какие устройства
|
||||
// действительно онлайн, и можем ошибочно сработать как «device→offline».
|
||||
|
||||
// Heartbeat не зависит от firstPassDone — стартуем сразу. Если desktop_id
|
||||
// ещё не получен (до loginAndSetup), первые тики просто скипнутся.
|
||||
startPeriodicPing();
|
||||
}
|
||||
|
||||
void NetworkService::startPeriodicSync() {
|
||||
@ -1628,6 +1676,33 @@ void NetworkService::startPeriodicSync() {
|
||||
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() {
|
||||
if (!m_running || !m_periodicSyncEnabled) return;
|
||||
|
||||
|
||||
@ -63,6 +63,13 @@ public slots:
|
||||
// health-счётчиком в runPeriodicSyncTick (правило #9).
|
||||
bool syncCurrentProfile();
|
||||
|
||||
// POST /api/v1/profile/ping — heartbeat: даёт серверу понять, что десктоп
|
||||
// онлайн. В теле уходят desktop_id и версия клиента. Вызывать периодически.
|
||||
void pingProfile();
|
||||
|
||||
// Включает периодический ping (kPingProfileMs). Идемпотентен.
|
||||
void startPeriodicPing();
|
||||
|
||||
// Включает периодическую синхронизацию статусов (60s). Должно быть вызвано
|
||||
// ОДИН раз для долгоживущего инстанса после firstPassDone от DeviceScreener.
|
||||
void startPeriodicSync();
|
||||
@ -142,8 +149,15 @@ private:
|
||||
QString m_monitoringToken;
|
||||
QString m_pathCurrentProfile;
|
||||
QString m_pathProfileGetInfo;
|
||||
QString m_pathProfilePing;
|
||||
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
|
||||
std::atomic<bool> m_syncInProgress{false}; // защита от наложения тиков
|
||||
@ -151,6 +165,11 @@ private:
|
||||
bool m_unhealthyEmitted = false; // чтобы не спамить syncHealthChanged
|
||||
static constexpr int kSyncFailureThreshold = 3;
|
||||
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) — общий для всех
|
||||
// инстансов NetworkService. Используется для сериализации PATCH-операций.
|
||||
@ -171,6 +190,8 @@ private:
|
||||
void scheduleNextRefresh();
|
||||
void scheduleNextStatusSync();
|
||||
bool runPeriodicSyncTick(); // делает один тик, возвращает true при успехе
|
||||
bool doPingProfile(); // тело ping без emit finished — для периодических тиков
|
||||
void scheduleNextPing();
|
||||
void syncDevicesFromServer(const QString &desktopId);
|
||||
void syncBankProfilesFromServer();
|
||||
void syncBanksIfEmpty();
|
||||
|
||||
@ -247,28 +247,36 @@ void AccountSettingsWindow::toggleProfileStatus() {
|
||||
|
||||
m_app.status = newStatus;
|
||||
showOverlay(isActive ? "Выключение профиля..." : "Включение профиля...");
|
||||
sendAppStatus(newStatus);
|
||||
|
||||
// При включении профиля — включаем на сервере все локально активные материалы
|
||||
// При включении профиля — материалы включаем ТОЛЬКО после успешного
|
||||
// включения bank_profile на сервере; иначе пользователь получит подряд
|
||||
// несколько одинаковых ошибок ("Invalid or disabled connection") от
|
||||
// независимых PATCH'ей.
|
||||
std::function<void()> onSuccess;
|
||||
if (!isActive) {
|
||||
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(m_deviceId, m_app.code);
|
||||
for (const auto &mat : materials) {
|
||||
if (mat.status == MaterialStatus::Active && !mat.materialId.isEmpty()) {
|
||||
auto *thread = new QThread;
|
||||
auto *net = new NetworkService;
|
||||
net->moveToThread(thread);
|
||||
net->addExtra("material_id", mat.materialId);
|
||||
net->addExtra("enable", true);
|
||||
NetworkService::connectErrorDialog(this, net);
|
||||
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
|
||||
connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||||
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||||
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||
thread->start();
|
||||
qDebug() << "[AccountSettings] Enabling material on server:" << mat.materialId;
|
||||
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) {
|
||||
if (mat.status == MaterialStatus::Active && !mat.materialId.isEmpty()) {
|
||||
auto *thread = new QThread;
|
||||
auto *net = new NetworkService;
|
||||
net->moveToThread(thread);
|
||||
net->addExtra("material_id", mat.materialId);
|
||||
net->addExtra("enable", true);
|
||||
NetworkService::connectErrorDialog(this, net);
|
||||
connect(thread, &QThread::started, net, &NetworkService::toggleMaterial);
|
||||
connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
@ -463,7 +471,7 @@ void AccountSettingsWindow::startGetCardInfo() {
|
||||
|
||||
// ── 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()) {
|
||||
qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << m_app.code;
|
||||
hideOverlay(true, "Сохранено локально", [this]() { updateStatusUI(); });
|
||||
@ -478,9 +486,10 @@ void AccountSettingsWindow::sendAppStatus(const QString &status) {
|
||||
net->addExtra("enable", enable);
|
||||
NetworkService::connectErrorDialog(this, net);
|
||||
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) {
|
||||
hideOverlay(true, "Успешно", [this]() { updateStatusUI(); });
|
||||
if (onSuccess) onSuccess();
|
||||
return;
|
||||
}
|
||||
// Правило #3: при неудачном включении — откатываем локаль в off и
|
||||
|
||||
@ -44,7 +44,7 @@ private:
|
||||
void startCheckPinCode();
|
||||
void startGetProfileInfo();
|
||||
void startGetCardInfo();
|
||||
void sendAppStatus(const QString &status);
|
||||
void sendAppStatus(const QString &status, std::function<void()> onSuccess = {});
|
||||
void toggleMaterialStatus(const MaterialInfo &account);
|
||||
void reloadCards();
|
||||
|
||||
|
||||
@ -92,11 +92,14 @@ void BankSettingsWindow::startCheckPinCode(const QString &appCode) {
|
||||
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;
|
||||
errData["status"] = "error";
|
||||
errData["event_type"] = type;
|
||||
errData["description"] = "Bank profile disabled";
|
||||
if (!transactionId.isEmpty())
|
||||
errData["transaction_id"] = transactionId;
|
||||
|
||||
QJsonObject obj;
|
||||
obj["type"] = "ERROR";
|
||||
@ -126,7 +129,7 @@ void sendAppStatus(const QString &deviceId, const QString &code, const QString &
|
||||
if (status != "active") {
|
||||
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByBank(deviceId, code);
|
||||
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);
|
||||
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;
|
||||
errData["status"] = "error";
|
||||
errData["event_type"] = type;
|
||||
errData["description"] = "Bank profile disabled";
|
||||
if (!transactionId.isEmpty())
|
||||
errData["transaction_id"] = transactionId;
|
||||
|
||||
QJsonObject obj;
|
||||
obj["type"] = "ERROR";
|
||||
@ -94,7 +97,7 @@ void sendBankStatus(const QString &deviceId, const QString &code, const QString
|
||||
if (status != "active") {
|
||||
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByBank(deviceId, code);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -107,7 +110,9 @@ void sendBankStatus(const QString &deviceId, const QString &code, const QString
|
||||
net->addExtra("enable", enable);
|
||||
QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
||||
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); },
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
@ -368,44 +373,44 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
|
||||
window->activateWindow();
|
||||
});
|
||||
|
||||
auto *deleteBtn = new QPushButton(this);
|
||||
deleteBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/delete.png"));
|
||||
deleteBtn->setIconSize(QSize(20, 20));
|
||||
deleteBtn->setFixedSize(28, 28);
|
||||
deleteBtn->setFlat(true);
|
||||
deleteBtn->setStyleSheet(
|
||||
"background-color: rgba(0, 0, 0, 0.5);"
|
||||
"border: none;"
|
||||
"border-radius: 4px;"
|
||||
);
|
||||
connect(deleteBtn, &QPushButton::clicked, this, [this]() {
|
||||
const QString deviceId = m_device.id;
|
||||
const QString deviceName = m_device.name;
|
||||
QPointer<DeviceWidget> guard(this);
|
||||
|
||||
QMessageBox msgBox(nullptr);
|
||||
msgBox.setWindowTitle(tr("Удалить устройство?"));
|
||||
msgBox.setText(
|
||||
"Будет удалено устройство " + deviceName +
|
||||
" и все связанные данные: банки, аккаунты, транзакции и события.");
|
||||
QPushButton *confirmBtn = msgBox.addButton(tr("Удалить"), QMessageBox::AcceptRole);
|
||||
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
|
||||
msgBox.setDefaultButton(cancelBtn);
|
||||
msgBox.setModal(true);
|
||||
msgBox.exec();
|
||||
|
||||
if (msgBox.clickedButton() == confirmBtn) {
|
||||
auto *thread = new QThread;
|
||||
auto *net = new NetworkService;
|
||||
net->setDeviceId(deviceId);
|
||||
net->moveToThread(thread);
|
||||
QObject::connect(thread, &QThread::started, net, &NetworkService::deleteDeviceFromServer);
|
||||
QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||||
QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||
thread->start();
|
||||
}
|
||||
});
|
||||
// auto *deleteBtn = new QPushButton(this);
|
||||
// deleteBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/delete.png"));
|
||||
// deleteBtn->setIconSize(QSize(20, 20));
|
||||
// deleteBtn->setFixedSize(28, 28);
|
||||
// deleteBtn->setFlat(true);
|
||||
// deleteBtn->setStyleSheet(
|
||||
// "background-color: rgba(0, 0, 0, 0.5);"
|
||||
// "border: none;"
|
||||
// "border-radius: 4px;"
|
||||
// );
|
||||
// connect(deleteBtn, &QPushButton::clicked, this, [this]() {
|
||||
// const QString deviceId = m_device.id;
|
||||
// const QString deviceName = m_device.name;
|
||||
// QPointer<DeviceWidget> guard(this);
|
||||
//
|
||||
// QMessageBox msgBox(nullptr);
|
||||
// msgBox.setWindowTitle(tr("Удалить устройство?"));
|
||||
// msgBox.setText(
|
||||
// "Будет удалено устройство " + deviceName +
|
||||
// " и все связанные данные: банки, аккаунты, транзакции и события.");
|
||||
// QPushButton *confirmBtn = msgBox.addButton(tr("Удалить"), QMessageBox::AcceptRole);
|
||||
// QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
|
||||
// msgBox.setDefaultButton(cancelBtn);
|
||||
// msgBox.setModal(true);
|
||||
// msgBox.exec();
|
||||
//
|
||||
// if (msgBox.clickedButton() == confirmBtn) {
|
||||
// auto *thread = new QThread;
|
||||
// auto *net = new NetworkService;
|
||||
// net->setDeviceId(deviceId);
|
||||
// net->moveToThread(thread);
|
||||
// QObject::connect(thread, &QThread::started, net, &NetworkService::deleteDeviceFromServer);
|
||||
// QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||||
// QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||||
// QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||
// thread->start();
|
||||
// }
|
||||
// });
|
||||
|
||||
auto *codeBtn = new QPushButton(this);
|
||||
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(deleteBtn);
|
||||
// topRightLayout->addWidget(deleteBtn);
|
||||
topRightLayout->addWidget(codeBtn);
|
||||
layout->addWidget(topRightWidget, 0, 0, Qt::AlignTop | Qt::AlignRight);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user