1
0
forked from BRT/arc
arc/services/net/NetworkService.cpp
slava 11e8f00e42 Add OCR utilities and backend integration for enhanced screen dump analysis:
- Introduce `OcrUtils` with fuzzy-matching logic for text recognition on screenshots.
- Implement dual-pass OCR to handle light-on-dark and dark-on-light text cases.
- Replace hardcoded "Подтверждаю" button tap logic with OCR-based detection and fallback geometry.
- Update `TaskDumpRecorder` to utilize `NetworkService` for uploading screen dumps, images, and metadata to the backend.
- Add support for `/monitoring/dump` endpoint in `NetworkService`.
- Enhance UI functionality with new error handling and progress feedback during dump creation and upload.
2026-04-30 19:17:17 +07:00

1503 lines
61 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "NetworkService.h"
#include <QFile>
#include <QUrlQuery>
#include <QHttpPart>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QThread>
#include <QSettings>
#include <QTimer>
#include <QTimeZone>
#include "dao/BankDAO.h"
#include "dao/GeneralLogDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/EventDAO.h"
#include "dao/SettingsDAO.h"
#include "dao/TransactionDAO.h"
#include "db/EventInfo.h"
// Убирает полный URL (хост:порт) из сообщений об ошибках, оставляя только
// путь эндпоинта и HTTP-код. Нужно, чтобы адрес сервера не попадал в
// логи/UI/general_logs/application.log.
static QString sanitizePath(const QString &fullUrl) {
const int schemeEnd = fullUrl.indexOf("://");
if (schemeEnd < 0) return fullUrl;
const int pathStart = fullUrl.indexOf('/', schemeEnd + 3);
return pathStart >= 0 ? fullUrl.mid(pathStart) : QStringLiteral("/");
}
static QString sanitizeNetError(QNetworkReply *reply, const QString &path) {
const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
const QString endpoint = sanitizePath(path);
if (httpCode > 0) {
return QStringLiteral("HTTP %1 %2").arg(httpCode).arg(endpoint);
}
// Сетевой уровень (не HTTP) — выводим enum-код вместо строки с URL.
return QStringLiteral("net_err=%1 %2").arg(static_cast<int>(reply->error())).arg(endpoint);
}
NetworkService::NetworkService(QObject *parent) : QObject(parent) {
const QSettings settings("config.ini", QSettings::IniFormat);
m_urlAccountUpdate = settings.value("net/accountUpdate").toString();
m_urlPaymentsGet = settings.value("net/paymentsGet").toString();
m_urlTransactionUpdate = settings.value("net/transactionUpdate").toString();
m_urlAppStatusUpdate = settings.value("net/appStatusUpdate").toString();
m_urlTransactionInsert = settings.value("net/transactionInsert").toString();
m_urlEventStatusUpdate = settings.value("net/eventStatusUpdate").toString();
m_apiBase = settings.value("net/apiBase").toString();
m_pathAuthLogin = settings.value("net/pathAuthLogin", "/api/v1/auth/login").toString();
m_pathAuthRefresh = settings.value("net/pathAuthRefresh", "/api/v1/auth/refresh_token").toString();
m_pathDesktopCreate = settings.value("net/pathDesktopCreate", "/api/v1/desktop/").toString();
m_pathDevice = settings.value("net/pathDevice", "/api/v1/device/").toString();
m_pathDeviceScreenshot= settings.value("net/pathDeviceScreenshot","/api/v1/device/add_screenshot/").toString();
m_pathBankProfile = settings.value("net/pathBankProfile", "/api/v1/profile/bank_profile").toString();
m_pathMaterial = settings.value("net/pathMaterial", "/api/v1/profile/material").toString();
m_pathGetTasks = settings.value("net/pathGetTasks", "/api/v1/task/get_tasks").toString();
m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString();
m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString();
m_pathMonitoringDump = settings.value("net/pathMonitoringDump", "/monitoring/dump").toString();
m_pathCurrentProfile = settings.value("net/pathCurrentProfile", "/api/v1/profile/current_profile").toString();
m_manager = new QNetworkAccessManager(this);
m_token = settings.value("common/token").toString();
m_accessToken = SettingsDAO::get("access_token");
}
NetworkService::~NetworkService() = default;
// FIXME доделать работу с экстра
void NetworkService::addExtra(const QString &key, const QVariant &value) {
m_extraData[key] = value;
}
void addFormField(QHttpMultiPart *multiPart, const QString &fieldName, const QString &fieldValue) {
QHttpPart formField;
formField.setHeader(
QNetworkRequest::ContentDispositionHeader,QStringLiteral("form-data; name=\"%1\"").arg(fieldName)
);
formField.setBody(fieldValue.toUtf8());
multiPart->append(formField);
}
void addFormMetadata(QHttpMultiPart *multiPart, const QByteArray &fieldValue) {
QHttpPart jsonPart;
jsonPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/json; charset=UTF-8");
jsonPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="metadata")");
jsonPart.setBody(fieldValue);
multiPart->append(jsonPart);
}
QJsonValue NetworkService::post(QHttpMultiPart *multiPart, const QString &path) {
const QUrl url(path);
const QNetworkRequest request(url);
QNetworkReply *reply = m_manager->post(request, multiPart);
multiPart->setParent(reply);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit();
});
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
QJsonValue result;
if (!timeout.isActive()) {
qCritical() << "Таймаут запроса:" << sanitizePath(path);
reply->abort();
} else if (reply->error() == QNetworkReply::NoError) {
const QByteArray raw = reply->readAll();
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
const QJsonObject root = doc.object();
if (root.value("success").toBool()) {
result = root.value("data");
} else {
qCritical() << "Ошибка:" << root;
}
}
} else {
qCritical() << "Ошибка:" << sanitizeNetError(reply, path);
}
reply->deleteLater();
return result;
}
QJsonArray NetworkService::get(const QString &path, const QMap<QString, QString> &params) {
QUrl url(path);
QUrlQuery query;
for (auto it = params.constBegin(); it != params.constEnd(); ++it) {
query.addQueryItem(it.key(), it.value());
}
url.setQuery(query);
const QNetworkRequest request(url);
QNetworkReply *reply = m_manager->get(request);
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> guard(reply);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit();
});
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
QJsonArray result;
if (!timeout.isActive()) {
qCritical() << "Таймаут запроса (GET):" << sanitizePath(path);
reply->abort();
} else if (reply->error() == QNetworkReply::NoError) {
const QByteArray raw = reply->readAll();
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
const QJsonObject root = doc.object();
if (root.value("success").toBool()) {
result = root.value("data").toArray();
} else {
qWarning() << "Ошибка:" << root;
}
}
} else {
qWarning() << "Ошибка:" << reply->errorString();
}
reply->deleteLater();
return result;
}
void NetworkService::postAccountsData() {
// TODO: заменить на новый API
emit finished();
}
void NetworkService::getPayments() {
QMap<QString, QString> params;
params["token"] = m_token;
const QJsonArray data = get(m_urlPaymentsGet, params);
for (QJsonValue item: data) {
QJsonObject obj = item.toObject();
EventInfo eventInfo;
eventInfo.status = EventStatus::Wait;
eventInfo.type = eventTypeFromString(obj["type"].toString());
eventInfo.externalId = obj["id"].isString()
? obj["id"].toString()
: QString::number(obj["id"].toInt());
eventInfo.timestamp = QDateTime::fromMSecsSinceEpoch(obj["timestamp"].toDouble(), QTimeZone::utc());
eventInfo.amount = obj["amount"].toDouble();
eventInfo.bankName = obj["bankName"].toString();
eventInfo.phone = obj["phone"].toString();
QString deviceId = obj["deviceId"].toString();
QString appName = obj["appName"].toString();
QString lastNumbers = obj["lastNumbers"].toString();
MaterialInfo account = MaterialDAO::findAppAccount(deviceId, appName, lastNumbers);
eventInfo.accountId = account.id;
eventInfo.deviceId = account.deviceId;
if (int id = EventDAO::insertEvent(eventInfo); id == -1) {
qCritical() << "Cant insert event: " << eventInfo.externalId;
}
}
if (!m_running) {
emit finished();
}
}
void NetworkService::postAppStatus() {
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
addFormMetadata(multiPart, m_json);
addFormField(multiPart, "token", m_token);
addFormField(multiPart, "androidUniqueName", m_deviceId);
const bool isSuccess = post(multiPart, m_urlAppStatusUpdate).toBool();
if (!isSuccess) {
qCritical() << "Cant post data: postAppStatus";
}
emit finished();
}
QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body, bool withAuth) {
const QUrl url(path);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (withAuth && !m_accessToken.isEmpty()) {
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
}
QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson());
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit();
});
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
QJsonValue result;
const QString shortPath = sanitizePath(path);
if (!timeout.isActive()) {
qCritical() << "Таймаут запроса (postJson):" << shortPath;
reply->abort();
GeneralLogDAO::insert("CRITICAL", "NetworkService",
"Timeout (POST): " + shortPath, {}, reqBodyStr, {});
result = QJsonValue::Undefined;
} else if (reply->error() != QNetworkReply::NoError) {
const QByteArray responseBody = reply->readAll();
const QString errSummary = sanitizeNetError(reply, path);
qCritical().noquote() << "Network error (POST):" << errSummary
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
<< "\n response body:" << responseBody;
GeneralLogDAO::insert("CRITICAL", "NetworkService",
"Network error (POST): " + errSummary, {},
reqBodyStr, QString::fromUtf8(responseBody));
result = QJsonValue::Undefined;
} else {
const QByteArray raw = reply->readAll();
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
const QJsonObject root = doc.object();
if (root["success"].toBool()) {
result = root["data"];
} else {
qCritical() << "API error:" << root;
GeneralLogDAO::insert("WARNING", "NetworkService",
"API error (POST): " + shortPath, {},
reqBodyStr, QString::fromUtf8(raw));
result = QJsonValue::Null;
}
}
}
reply->deleteLater();
return result;
}
void NetworkService::loginAndSetup() {
const QString apiKey = SettingsDAO::get("api_key");
// Шаг 1: логин
QJsonObject loginBody;
loginBody["api_key"] = apiKey;
const QJsonValue loginResult = postJson(m_apiBase + m_pathAuthLogin, loginBody);
if (loginResult.isUndefined()) {
emit finishedWithResult(-1); // ошибка сети
emit finished();
return;
}
if (loginResult.isNull()) {
emit finishedWithResult(0); // неверный api_key
emit finished();
return;
}
const QJsonObject loginData = loginResult.toObject();
m_accessToken = loginData["access_token"].toString();
if (m_accessToken.isEmpty() || !loginData.contains("refresh_token") || !loginData.contains("expires_in")) {
qCritical() << "loginAndSetup: ответ не содержит обязательных полей (access_token/refresh_token/expires_in)";
emit finishedWithResult(-1);
emit finished();
return;
}
SettingsDAO::set("access_token", m_accessToken);
SettingsDAO::set("refresh_token", loginData["refresh_token"].toString());
const qint64 expiresIn = static_cast<qint64>(loginData["expires_in"].toDouble());
SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn);
// Шаг 2: получаем актуальный desktop с сервера
QString desktopId = SettingsDAO::get("desktop_id");
// Всегда проверяем список десктопов — desktop_id мог устареть или быть удалён
const QJsonValue existingDesktops = getJson(m_apiBase + m_pathDesktopCreate, true);
if (!existingDesktops.isUndefined() && !existingDesktops.isNull() && existingDesktops.isArray()) {
const QJsonArray desktopsArray = existingDesktops.toArray();
QString foundId;
QString foundProfileId;
for (const QJsonValue &dVal : desktopsArray) {
const QJsonObject d = dVal.toObject();
if (d["hidden"].toBool(false)) continue;
foundId = d["id"].toString();
foundProfileId = d["profile_id"].toString();
if (!foundId.isEmpty()) break;
}
if (!foundId.isEmpty() && foundId != desktopId) {
// Нашли актуальный десктоп, отличается от сохранённого — обновляем
qDebug() << "[loginAndSetup] desktop_id updated:" << desktopId << "->" << foundId;
desktopId = foundId;
SettingsDAO::set("desktop_id", desktopId);
SettingsDAO::set("profile_id", foundProfileId);
} else if (!foundId.isEmpty()) {
qDebug() << "[loginAndSetup] desktop_id confirmed:" << desktopId;
}
}
if (desktopId.isEmpty()) {
// Десктопов на сервере нет — создаём новый
QJsonObject desktopBody;
desktopBody["blue_token"] = QJsonValue::Null;
desktopBody["name"] = QJsonValue::Null;
desktopBody["contact"] = QJsonValue::Null;
const QSettings settings("config.ini", QSettings::IniFormat);
desktopBody["app_version"] = settings.value("common/version").toString();
const QJsonValue desktopResult = postJson(m_apiBase + m_pathDesktopCreate, desktopBody, true);
if (desktopResult.isUndefined() || desktopResult.isNull()) {
emit finishedWithResult(-1);
emit finished();
return;
}
const QJsonObject desktopData = desktopResult.toObject();
desktopId = desktopData["id"].toString();
SettingsDAO::set("desktop_id", desktopId);
SettingsDAO::set("profile_id", desktopData["profile_id"].toString());
}
// Шаг 3: синхронизация устройств с сервера
syncDevicesFromServer(desktopId);
// Шаг 4: синхронизация банковских профилей и материалов с сервера
syncBankProfilesFromServer();
// Шаг 5: убран (bank_participator_name_ru приходит в задаче)
// Шаг 6: синхронизация статусов профилей/материалов с учётом онлайн-устройств
syncCurrentProfile();
// Шаг 7: отправляем OFFLINE статус всех устройств (DeviceScreener обновит на ONLINE при обнаружении)
{
const QList<DeviceInfo> allDevices = DeviceDAO::getAllDevices(false);
for (const DeviceInfo &d : allDevices) {
if (d.apiId.isEmpty()) continue;
QJsonObject body;
body["status"] = "OFFLINE";
body["bank_profile_id"] = QJsonValue::Null;
body["battery"] = d.battery;
body["update_time"] = QDateTime::currentSecsSinceEpoch();
patchJson(m_apiBase + m_pathDevice + d.apiId, body, true);
qDebug() << "[loginAndSetup] sent OFFLINE for device" << d.id << d.apiId;
}
}
emit finishedWithResult(1);
emit finished();
}
void NetworkService::postEventStatus() {
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
addFormMetadata(multiPart, m_json);
const bool isSuccess = post(multiPart, m_urlEventStatusUpdate).toBool();
if (!isSuccess) {
qCritical() << "Cant post data: postEventStatus";
}
emit finished();
}
void NetworkService::updateTransactionData() {
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
addFormMetadata(multiPart, m_json);
addFormField(multiPart, "token", m_token);
addFormField(multiPart, "androidUniqueName", m_deviceId);
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
const bool isSuccess = post(multiPart, m_urlTransactionUpdate).toBool();
if (!isSuccess) {
qCritical() << "Cant update transaction: updateTransactionData";
}
emit finished();
}
void NetworkService::insertTransactionData() {
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
addFormMetadata(multiPart, m_json);
addFormField(multiPart, "token", m_token);
addFormField(multiPart, "androidUniqueName", m_deviceId);
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
const int id = post(multiPart, m_urlTransactionInsert).toInt();
if (id > 0) {
const int transactionId = m_extraData["transactionId"].toInt();
if (!TransactionDAO::updateExternalTransactionId(transactionId, id)) {
qCritical() << "Cant update transaction id: " << id;
}
}
emit finished();
}
QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &body, bool withAuth) {
const QUrl url(path);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (withAuth && !m_accessToken.isEmpty()) {
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
}
QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson());
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
QJsonValue result;
if (!timeout.isActive()) {
const QString shortPath = sanitizePath(path);
qCritical() << "Таймаут запроса (PATCH):" << shortPath;
reply->abort();
GeneralLogDAO::insert("CRITICAL", "NetworkService",
"Timeout (PATCH): " + shortPath, {}, reqBodyStr, {});
} else if (reply->error() != QNetworkReply::NoError) {
const QByteArray responseBody = reply->readAll();
const QString errSummary = sanitizeNetError(reply, path);
qCritical().noquote() << "Network error (PATCH):" << errSummary
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
<< "\n response body:" << responseBody;
GeneralLogDAO::insert("CRITICAL", "NetworkService",
"Network error (PATCH): " + errSummary, {},
reqBodyStr, QString::fromUtf8(responseBody));
} else {
const QByteArray raw = reply->readAll();
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
const QJsonObject root = doc.object();
if (root["success"].toBool()) {
result = root["data"];
} else {
qCritical() << "API error (PATCH):" << root;
GeneralLogDAO::insert("WARNING", "NetworkService",
"API error (PATCH): " + sanitizePath(path), {},
reqBodyStr, QString::fromUtf8(raw));
}
}
}
reply->deleteLater();
return result;
}
QJsonValue NetworkService::deleteJson(const QString &path, bool withAuth) {
const QUrl url(path);
QNetworkRequest request(url);
if (withAuth && !m_accessToken.isEmpty()) {
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
}
QNetworkReply *reply = m_manager->deleteResource(request);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
QJsonValue result;
if (!timeout.isActive()) {
qCritical() << "Таймаут запроса (DELETE):" << sanitizePath(path);
reply->abort();
result = QJsonValue::Undefined;
} else if (reply->error() != QNetworkReply::NoError) {
qCritical().noquote() << "Network error (DELETE):" << sanitizeNetError(reply, path)
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId);
result = QJsonValue::Undefined;
} else {
const QByteArray raw = reply->readAll();
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
const QJsonObject root = doc.object();
if (root["success"].toBool()) {
result = root["data"];
} else {
qCritical() << "API error (DELETE):" << root;
result = QJsonValue::Null;
}
}
}
reply->deleteLater();
return result;
}
QJsonValue NetworkService::getJson(const QString &path, bool withAuth) {
const QUrl url(path);
QNetworkRequest request(url);
if (withAuth && !m_accessToken.isEmpty()) {
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
}
QNetworkReply *reply = m_manager->get(request);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit();
});
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
QJsonValue result;
if (!timeout.isActive()) {
qCritical() << "Таймаут запроса (getJson):" << sanitizePath(path);
reply->abort();
result = QJsonValue::Undefined;
} else if (reply->error() != QNetworkReply::NoError) {
qCritical().noquote() << "Network error (GET):" << sanitizeNetError(reply, path)
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId);
result = QJsonValue::Undefined;
} else {
const QByteArray raw = reply->readAll();
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
const QJsonObject root = doc.object();
if (root["success"].toBool()) {
result = root["data"];
} else {
qCritical() << "API error (GET):" << root;
result = QJsonValue::Null;
}
}
}
reply->deleteLater();
return result;
}
void NetworkService::syncBankProfilesFromServer() {
const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true);
if (profileResult.isUndefined() || profileResult.isNull()) {
qWarning() << "[syncBankProfilesFromServer] failed to get current_profile";
return;
}
const QJsonObject profileData = profileResult.toObject();
qDebug() << "[syncBankProfilesFromServer] profileData keys:" << profileData.keys()
<< "bank_profiles isArray:" << profileData["bank_profiles"].isArray()
<< "bank_profiles size:" << profileData["bank_profiles"].toArray().size();
const QJsonArray bankProfiles = profileData["bank_profiles"].toArray();
// Маппинг apiId → локальный device_id
const QList<DeviceInfo> allDevices = DeviceDAO::getAllDevices(false);
QMap<QString, QString> apiIdToLocalId;
for (const DeviceInfo &d : allDevices) {
if (!d.apiId.isEmpty()) {
apiIdToLocalId[d.apiId] = d.id;
}
}
const QSettings configSettings("config.ini", QSettings::IniFormat);
for (const QJsonValue &bpVal : bankProfiles) {
const QJsonObject bp = bpVal.toObject();
const QString bpId = bp["id"].toString();
const QString bankName = bp["bank_name"].toString();
const QString bpState = bp["state"].toString();
QString phone = bp["phone_number"].toString();
phone.remove(' ');
phone.remove(QChar(0x00A0));
const QString firstName = bp["first_name"].toString();
const QString middleName = bp["middle_name"].toString();
const QString lastName = bp["last_name"].toString();
// Конвертируем числовой ISO 4217 код в строковый
QString currency;
if (!bp["currency"].isNull()) {
static const QMap<int, QString> isoCurrencyMap = {
{643, "RUB"}, {840, "USD"}, {410, "KRW"}, {944, "AZN"}, {32, "ARS"}, {972, "TJS"},
{978, "EUR"}, {826, "GBP"}, {156, "CNY"}, {392, "JPY"}, {949, "TRY"},
};
const int code = bp["currency"].toInt();
currency = isoCurrencyMap.value(code, QString::number(code));
}
const QString serverDeviceId = bp["device_id"].toString(); // API UUID устройства
if (bpId.isEmpty() || bankName.isEmpty()) continue;
// Определяем локальный device_id по API UUID
const QString localDeviceId = apiIdToLocalId.value(serverDeviceId);
if (localDeviceId.isEmpty()) {
qDebug() << "[syncBankProfilesFromServer] device not found locally for api_id" << serverDeviceId << ", skipping bp" << bpId;
continue;
}
const QString fullName = (firstName + " " + (middleName.isEmpty() ? "" : middleName + " ") + lastName).trimmed();
BankProfileInfo app = BankProfileDAO::getApplication(localDeviceId, bankName);
if (app.id < 0) {
// Приложение не обнаружено на устройстве — создаём запись
BankProfileInfo newApp;
newApp.deviceId = localDeviceId;
newApp.code = bankName;
newApp.name = configSettings.value(bankName + "/name", bankName).toString();
newApp.package = configSettings.value(bankName + "/android_package_name").toString();
newApp.status = (bpState == "disabled") ? "off" : "active";
newApp.install = true;
newApp.pinCodeChecked = false;
newApp.bankProfileId = bpId;
newApp.fullName = fullName;
newApp.phone = phone;
newApp.currency = currency.isEmpty()
? configSettings.value(bankName + "/currency").toString()
: currency;
if (BankProfileDAO::upsertApplication(newApp)) {
app = BankProfileDAO::getApplication(localDeviceId, bankName);
qDebug() << "[syncBankProfilesFromServer] created app" << app.id << bankName << "device" << localDeviceId;
} else {
qWarning() << "[syncBankProfilesFromServer] failed to create app" << bankName << "device" << localDeviceId;
continue;
}
} else {
// Application существует — привязываем bank_profile_id если нужно
if (app.bankProfileId != bpId) {
BankProfileDAO::updateBankProfileId(app.id, bpId);
qDebug() << "[syncBankProfilesFromServer] linked bp" << bpId << "to app" << app.id;
}
if (app.fullName.isEmpty() || app.phone.isEmpty()) {
BankProfileDAO::updateProfileInfo(app.id, fullName, phone);
}
}
// Синхронизируем materials → accounts
const QJsonArray materials = bp["materials"].toArray();
for (const QJsonValue &matVal : materials) {
const QJsonObject mat = matVal.toObject();
const QString matId = mat["id"].toString();
const QString cardNumber = mat["card_number"].toString();
if (matId.isEmpty()) continue;
// Уже есть account с этим material_id — пропускаем
const MaterialInfo existing = MaterialDAO::findByMaterialId(matId);
if (existing.id >= 0) continue;
const QString lastNumbers = cardNumber.length() >= 4 ? cardNumber.right(4) : cardNumber;
// Ищем существующий account по last_numbers
MaterialInfo acc = MaterialDAO::findAppAccount(localDeviceId, bankName, lastNumbers);
if (acc.id >= 0) {
if (acc.materialId.isEmpty()) {
MaterialDAO::updateMaterialId(acc.id, matId);
if (acc.cardNumber.isEmpty() && !cardNumber.isEmpty()) {
MaterialDAO::updateCardNumber(acc.id, cardNumber);
}
qDebug() << "[syncBankProfilesFromServer] linked material" << matId << "to account" << acc.id;
}
} else if (!lastNumbers.isEmpty()) {
// Account не существует — создаём
MaterialInfo newAcc;
newAcc.deviceId = localDeviceId;
newAcc.appCode = bankName;
newAcc.lastNumbers = lastNumbers;
newAcc.cardNumber = cardNumber;
newAcc.materialId = matId;
newAcc.currency = currency.isEmpty()
? configSettings.value(bankName + "/currency").toString()
: currency;
newAcc.status = MaterialStatus::Active;
newAcc.updateTime = QDateTime::currentDateTime();
if (MaterialDAO::upsertAccount(newAcc)) {
qDebug() << "[syncBankProfilesFromServer] created account for material" << matId << "card" << lastNumbers;
} else {
qWarning() << "[syncBankProfilesFromServer] failed to create account for material" << matId;
}
}
}
}
qDebug() << "[syncBankProfilesFromServer] done";
}
void NetworkService::syncBanksIfEmpty() {
const QList<BankInfo> existing = BankDAO::getAll();
if (!existing.isEmpty()) {
qDebug() << "[syncBanksIfEmpty] banks table has" << existing.size() << "entries, skipping";
return;
}
qDebug() << "[syncBanksIfEmpty] banks table empty, downloading...";
QNetworkRequest request(QUrl("https://api.brtservice.io/api/rest/info/"));
QNetworkReply *reply = m_manager->get(request);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
if (!timeout.isActive()) {
qWarning() << "[syncBanksIfEmpty] timeout";
reply->abort();
reply->deleteLater();
return;
}
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[syncBanksIfEmpty] error:" << reply->errorString();
reply->deleteLater();
return;
}
const QByteArray data = reply->readAll();
reply->deleteLater();
const QJsonDocument doc = QJsonDocument::fromJson(data);
QJsonArray array;
if (doc.isArray()) {
array = doc.array();
} else if (doc.isObject() && doc.object().contains("data")) {
array = doc.object()["data"].toArray();
}
QList<BankInfo> banks;
for (const QJsonValue &val : array) {
const QJsonObject obj = val.toObject();
BankInfo bank;
bank.alias = obj["alias"].toString();
bank.name = obj["name"].toString();
bank.nspkName = obj["nspk_name"].toString();
if (!bank.alias.isEmpty()) {
banks.append(bank);
}
}
if (!banks.isEmpty()) {
BankDAO::replaceAll(banks);
qDebug() << "[syncBanksIfEmpty] synced" << banks.size() << "banks";
} else {
qWarning() << "[syncBanksIfEmpty] no banks in response";
}
}
void NetworkService::syncDevicesFromServer(const QString &desktopId) {
if (desktopId.isEmpty()) return;
const QJsonValue devicesResult = getJson(m_apiBase + m_pathDevice + desktopId, true);
if (devicesResult.isUndefined() || devicesResult.isNull() || !devicesResult.isArray()) {
qWarning() << "[syncDevicesFromServer] failed to get devices for desktop" << desktopId;
return;
}
const QJsonArray devicesArray = devicesResult.toArray();
const QList<DeviceInfo> allDevices = DeviceDAO::getAllDevices(false);
QSet<QString> processedNames; // защита от дублей с сервера
for (const QJsonValue &devVal : devicesArray) {
const QJsonObject dev = devVal.toObject();
const QString apiId = dev["id"].toString();
const QString name = dev["name"].toString();
if (apiId.isEmpty()) continue;
// Пропускаем дубли по name
if (processedNames.contains(name)) {
qDebug() << "[syncDevicesFromServer] skipping duplicate:" << apiId << name;
continue;
}
processedNames.insert(name);
// Проверяем: есть ли уже устройство с таким apiId в локальной БД
bool apiIdExists = false;
for (const DeviceInfo &d : allDevices) {
if (d.apiId == apiId) { apiIdExists = true; break; }
}
if (apiIdExists) continue;
const DeviceInfo existing = DeviceDAO::findByName(name);
if (!existing.id.isEmpty() && !existing.apiId.isEmpty()) {
continue;
}
if (!existing.id.isEmpty() && existing.apiId.isEmpty()) {
DeviceDAO::updateApiId(existing.id, apiId);
qDebug() << "[syncDevicesFromServer] linked apiId" << apiId << "to device" << existing.id;
continue;
}
// Устройства нет в БД — создаём одну запись (DeviceScreener потом подхватит)
DeviceInfo deviceInfo;
deviceInfo.apiId = apiId;
deviceInfo.name = name;
deviceInfo.status = dev["status"].toString();
deviceInfo.screenWidth = dev["screen_width"].toInt();
deviceInfo.screenHeight = dev["screen_height"].toInt();
deviceInfo.battery = dev["battery"].toInt();
deviceInfo.id = apiId;
if (DeviceDAO::upsertDevice(deviceInfo)) {
qDebug() << "[syncDevicesFromServer] restored device:" << apiId << name;
} else {
qWarning() << "[syncDevicesFromServer] failed to restore device:" << apiId;
}
}
}
void NetworkService::syncCurrentProfile() {
// 1. Определяем онлайн-устройства по БД (DeviceScreener обновит позже)
const QList<DeviceInfo> allDevices = DeviceDAO::getAllDevices(false);
QSet<QString> onlineDeviceIds;
for (const DeviceInfo &dev : allDevices) {
if (dev.status == "device" && !dev.image.isEmpty()) {
onlineDeviceIds.insert(dev.id);
}
}
qDebug() << "[syncCurrentProfile] online devices:" << onlineDeviceIds;
// 3. GET current_profile
const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true);
if (profileResult.isUndefined() || profileResult.isNull()) {
qWarning() << "[syncCurrentProfile] failed to get current_profile";
return;
}
const QJsonObject profileData = profileResult.toObject();
// 4. Сохраняем profile_id
const QString profileId = profileData["id"].toString();
if (!profileId.isEmpty()) {
SettingsDAO::set("profile_id", profileId);
}
// 5. Синхронизируем bank_profiles и materials
const QJsonArray bankProfiles = profileData["bank_profiles"].toArray();
for (const QJsonValue &bpVal : bankProfiles) {
const QJsonObject bp = bpVal.toObject();
const QString bpId = bp["id"].toString();
const QString serverBpState = bp["state"].toString(); // "enabled" / "disabled"
// Находим applications с этим bank_profile_id
const QList<BankProfileInfo> apps = BankProfileDAO::findAllByBankProfileId(bpId);
for (const BankProfileInfo &app : apps) {
const bool deviceOnline = onlineDeviceIds.contains(app.deviceId);
const QString localStatus = app.status; // "active" / "off"
const QString serverStatus = (serverBpState == "disabled") ? "off" : "active";
if (!deviceOnline) {
// Офлайн: если на сервере enabled → PATCH disable
if (serverBpState != "disabled") {
qDebug() << "[syncCurrentProfile] device" << app.deviceId << "offline, disabling bank_profile" << bpId;
patchJson(m_apiBase + m_pathBankProfile + "/" + bpId + "/false", QJsonObject(), true);
}
} else {
// Онлайн: если локальный status != серверный → PATCH локальное значение на сервер
if (localStatus != serverStatus) {
const QString enableStr = (localStatus == "active") ? "true" : "false";
qDebug() << "[syncCurrentProfile] device" << app.deviceId << "online, syncing bank_profile" << bpId << "to" << enableStr;
patchJson(m_apiBase + m_pathBankProfile + "/" + bpId + "/" + enableStr, QJsonObject(), true);
}
}
}
// 6. Синхронизируем materials
const QJsonArray materials = bp["materials"].toArray();
for (const QJsonValue &matVal : materials) {
const QJsonObject mat = matVal.toObject();
const QString matId = mat["id"].toString();
const QString serverMatState = mat["state"].toString();
const MaterialInfo account = MaterialDAO::findByMaterialId(matId);
if (account.id < 0) continue; // не найден в локальной БД
const bool deviceOnline = onlineDeviceIds.contains(account.deviceId);
const QString localStatus = materialStatusToString(account.status);
const QString serverStatus = (serverMatState == "disabled") ? "off" : "active";
if (!deviceOnline) {
if (serverMatState != "disabled") {
qDebug() << "[syncCurrentProfile] device" << account.deviceId << "offline, disabling material" << matId;
patchJson(m_apiBase + m_pathMaterial + "/" + matId + "/false", QJsonObject(), true);
}
} else {
if (localStatus != serverStatus) {
const QString enableStr = (localStatus == "active") ? "true" : "false";
qDebug() << "[syncCurrentProfile] device" << account.deviceId << "online, syncing material" << matId << "to" << enableStr;
patchJson(m_apiBase + m_pathMaterial + "/" + matId + "/" + enableStr, QJsonObject(), true);
}
}
}
}
qDebug() << "[syncCurrentProfile] done";
}
void NetworkService::postBankProfile() {
if (!ensureValidToken()) { emit finished(); return; }
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
qDebug() << "[NetworkService] postBankProfile payload:" << QJsonDocument(body).toJson(QJsonDocument::Compact);
const QJsonValue result = postJson(m_apiBase + m_pathBankProfile, body, true);
if (result.isNull() || result.isUndefined()) {
qWarning() << "[NetworkService] postBankProfile failed";
} else {
const QJsonObject data = result.toObject();
const QString bankProfileId = data["id"].toString();
const int appId = m_extraData.value("app_id").toInt();
if (!bankProfileId.isEmpty() && appId > 0) {
BankProfileDAO::updateBankProfileId(appId, bankProfileId);
qDebug() << "[NetworkService] postBankProfile saved bank_profile_id:" << bankProfileId;
}
}
emit finished();
}
void NetworkService::patchBankProfile() {
if (!ensureValidToken()) { emit finished(); return; }
const QString bankProfileId = m_extraData.value("bank_profile_id").toString();
if (bankProfileId.isEmpty()) {
qWarning() << "[NetworkService] patchBankProfile: bank_profile_id is empty";
emit finished();
return;
}
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
patchJson(m_apiBase + m_pathBankProfile + "/" + bankProfileId, body, true);
qDebug() << "[NetworkService] patchBankProfile done for id:" << bankProfileId;
emit finished();
}
void NetworkService::toggleMaterial() {
if (!ensureValidToken()) { emit finishedWithResult(-1); emit finished(); return; }
const QString materialId = m_extraData.value("material_id").toString();
const bool enable = m_extraData.value("enable").toBool();
if (materialId.isEmpty()) {
qWarning() << "[NetworkService] toggleMaterial: material_id is empty";
emit finishedWithResult(-1); emit finished();
return;
}
const QString enableStr = enable ? "true" : "false";
const QString path = m_apiBase + m_pathMaterial + "/" + materialId + "/" + enableStr;
const QJsonValue result = patchJson(path, QJsonObject(), true);
if (result.isNull() && result.isUndefined()) {
qWarning() << "[NetworkService] toggleMaterial failed:" << materialId;
emit finishedWithResult(-1);
} else {
qDebug() << "[NetworkService] toggleMaterial done:" << materialId << "enable=" << enable;
emit finishedWithResult(0);
}
emit finished();
}
void NetworkService::toggleBankProfile() {
if (!ensureValidToken()) { emit finishedWithResult(-1); emit finished(); return; }
const QString bankProfileId = m_extraData.value("bank_profile_id").toString();
const bool enable = m_extraData.value("enable").toBool();
if (bankProfileId.isEmpty()) {
qWarning() << "[NetworkService] toggleBankProfile: bank_profile_id is empty";
emit finishedWithResult(-1); emit finished();
return;
}
const QString enableStr = enable ? "true" : "false";
const QString path = m_apiBase + m_pathBankProfile + "/" + bankProfileId + "/" + enableStr;
const QJsonValue result = patchJson(path, QJsonObject(), true);
if (result.isNull() && result.isUndefined()) {
qWarning() << "[NetworkService] toggleBankProfile failed:" << bankProfileId;
emit finishedWithResult(-1);
} else {
qDebug() << "[NetworkService] toggleBankProfile done:" << bankProfileId << "enable=" << enable;
emit finishedWithResult(0);
}
emit finished();
}
void NetworkService::postMaterial() {
if (!ensureValidToken()) { emit finished(); return; }
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
qDebug() << "[NetworkService] postMaterial body:" << QJsonDocument(body).toJson(QJsonDocument::Compact);
const QJsonValue result = postJson(m_apiBase + m_pathMaterial, body, true);
if (result.isNull() || result.isUndefined()) {
qWarning() << "[NetworkService] postMaterial failed, card:" << body["card_number"].toString();
} else {
const QString materialId = result.toObject()["id"].toString();
const int accountId = m_extraData.value("account_id").toInt();
if (!materialId.isEmpty() && accountId > 0) {
MaterialDAO::updateMaterialId(accountId, materialId);
}
qDebug() << "[NetworkService] postMaterial ok, card:" << body["card_number"].toString() << "material_id:" << materialId;
}
emit finished();
}
void NetworkService::postDeviceScreenshot() {
if (!ensureValidToken()) { emit finished(); return; }
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
if (device.apiId.isEmpty()) {
qWarning() << "[NetworkService] postDeviceScreenshot: no api_id for" << m_deviceId;
emit finished();
return;
}
if (m_json.isEmpty()) {
qWarning() << "[NetworkService] postDeviceScreenshot: no image data";
emit finished();
return;
}
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
filePart.setHeader(
QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"file\"; filename=\"%1.jpg\"").arg(m_deviceId)
);
filePart.setBody(m_json);
multiPart->append(filePart);
const QUrl url(m_apiBase + m_pathDeviceScreenshot + device.apiId);
QNetworkRequest request(url);
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
QNetworkReply *reply = m_manager->post(request, multiPart);
multiPart->setParent(reply);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit();
});
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
if (!timeout.isActive()) {
qWarning() << "[NetworkService] postDeviceScreenshot timeout for device" << device.apiId;
reply->abort();
} else if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[NetworkService] postDeviceScreenshot failed:" << reply->errorString();
} else {
qDebug() << "[NetworkService] postDeviceScreenshot ok for device" << device.apiId;
}
reply->deleteLater();
emit finished();
}
void NetworkService::createDevice() {
const QString desktopId = SettingsDAO::get("desktop_id");
if (desktopId.isEmpty()) {
qWarning() << "createDevice: desktop_id not set";
emit finished();
return;
}
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
const QJsonValue result = postJson(m_apiBase + m_pathDevice, body, true);
if (!result.isNull() && !result.isUndefined()) {
const QString apiId = result.toObject()["id"].toString();
if (!apiId.isEmpty() && !m_deviceId.isEmpty()) {
DeviceDAO::updateApiId(m_deviceId, apiId);
qDebug() << "createDevice: saved api_id" << apiId << "for" << m_deviceId;
}
}
emit finished();
}
void NetworkService::updateDevice() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
if (device.apiId.isEmpty()) {
qWarning() << "updateDevice: api_id not set for" << m_deviceId;
emit finished();
return;
}
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
patchJson(m_apiBase + m_pathDevice + device.apiId, body, true);
emit finished();
}
bool NetworkService::refreshToken() {
const QString storedRefreshToken = SettingsDAO::get("refresh_token");
if (storedRefreshToken.isEmpty()) {
qWarning() << "refreshToken: refresh_token отсутствует";
return false;
}
QJsonObject body;
body["refresh_token"] = storedRefreshToken;
const QJsonValue result = postJson(m_apiBase + m_pathAuthRefresh, body);
if (result.isNull() || result.isUndefined()) {
qCritical() << "refreshToken: не удалось обновить токен";
if (!m_tokenErrorShown) {
m_tokenErrorShown = true;
emit tokenRefreshFailed();
}
return false;
}
const QJsonObject data = result.toObject();
m_accessToken = data["access_token"].toString();
if (m_accessToken.isEmpty() || !data.contains("refresh_token") || !data.contains("expires_in")) {
qCritical() << "refreshToken: ответ не содержит обязательных полей";
return false;
}
SettingsDAO::set("access_token", m_accessToken);
SettingsDAO::set("refresh_token", data["refresh_token"].toString());
const qint64 expiresIn = static_cast<qint64>(data["expires_in"].toDouble());
SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn);
m_tokenErrorShown = false; // сбрасываем если снова заработало
qDebug() << "refreshToken: токен обновлён, истекает через" << expiresIn << "сек";
return true;
}
bool NetworkService::ensureValidToken() {
const qint64 expiresAt = SettingsDAO::getLong("token_expires_at");
const qint64 now = QDateTime::currentSecsSinceEpoch();
// Обновляем заранее — за 30 минут до истечения
if (expiresAt - now < 30 * 60) {
qDebug() << "ensureValidToken: токен истекает через" << (expiresAt - now) << "сек, обновляем";
return refreshToken();
}
return true;
}
void NetworkService::scheduleNextRefresh() {
if (!m_running) return;
const qint64 expiresAt = SettingsDAO::getLong("token_expires_at");
const qint64 now = QDateTime::currentSecsSinceEpoch();
// Обновляем за 30 минут до истечения; если уже пора — сразу
const qint64 secsUntilRefresh = qMax<qint64>(0, expiresAt - now - 30 * 60);
qDebug() << "scheduleNextRefresh: следующий рефреш через" << secsUntilRefresh << "сек"
<< "(" << secsUntilRefresh / 3600 << "ч" << (secsUntilRefresh % 3600) / 60 << "мин)";
QTimer::singleShot(secsUntilRefresh * 1000, this, [this]() {
if (!m_running) return;
if (refreshToken()) {
scheduleNextRefresh(); // успех — планируем следующий
} else {
// Ошибка — повторяем через 60 секунд
qWarning() << "scheduleNextRefresh: повтор через 60 сек";
QTimer::singleShot(60 * 1000, this, [this]() {
scheduleNextRefresh();
});
}
});
}
void NetworkService::getTasks(const QStringList &materialIds) {
if (m_fetchingTasks) {
qDebug() << "[NetworkService] getTasks skipped: already fetching";
return;
}
m_fetchingTasks = true;
if (!ensureValidToken()) {
m_fetchingTasks = false;
return;
}
const QString accessToken = SettingsDAO::get("access_token");
QUrl url(m_apiBase + m_pathGetTasks);
QUrlQuery urlQuery;
const QStringList ids = materialIds.isEmpty() ? MaterialDAO::getActiveMaterialIds() : materialIds;
if (ids.isEmpty()) {
m_fetchingTasks = false;
return;
}
for (const QString &mid : ids) {
urlQuery.addQueryItem("materials_id", mid);
}
url.setQuery(urlQuery);
QNetworkRequest request(url);
request.setRawHeader("Authorization", ("Bearer " + accessToken).toUtf8());
QNetworkReply *reply = m_manager->get(request);
// Таймаут 30 сек: если ответ не пришёл, abort reply чтобы m_fetchingTasks не залип
auto *timeoutTimer = new QTimer(this);
timeoutTimer->setSingleShot(true);
connect(timeoutTimer, &QTimer::timeout, reply, [reply]() { reply->abort(); });
connect(reply, &QNetworkReply::finished, timeoutTimer, &QTimer::stop);
connect(reply, &QNetworkReply::finished, timeoutTimer, &QObject::deleteLater);
timeoutTimer->start(30000);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
m_fetchingTasks = false;
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[NetworkService] getTasks error:" << reply->errorString()
<< "status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
return;
}
const QByteArray raw = reply->readAll();
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
qWarning() << "[NetworkService] getTasks: invalid JSON";
return;
}
const QJsonObject root = doc.object();
if (!root.value("success").toBool()) {
qWarning() << "[NetworkService] getTasks: success=false" << root;
return;
}
emit tasksReceived(root.value("data").toArray());
});
}
void NetworkService::postTaskResult() {
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
const QJsonValue result = postJson(m_apiBase + m_pathTaskResult, body, true);
if (result.isUndefined()) {
qWarning() << "[NetworkService] postTaskResult failed, body:" << QJsonDocument(body).toJson(QJsonDocument::Compact);
emit finishedWithResult(-1);
} else {
emit finishedWithResult(0);
}
emit finished();
}
void NetworkService::postMonitoringLog(const QString &type, const QString &event,
const QString &message, const QByteArray &image) {
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
addFormField(multiPart, "type", type.toUpper());
addFormField(multiPart, "event", event);
addFormField(multiPart, "message", message);
const int internalTaskId = m_extraData.value("internal_task_id").toInt();
if (internalTaskId > 0) {
addFormField(multiPart, "internal_task_id", QString::number(internalTaskId));
}
if (!image.isEmpty()) {
QHttpPart imagePart;
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
imagePart.setHeader(
QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"image\"; filename=\"screenshot.png\"")
);
imagePart.setBody(image);
multiPart->append(imagePart);
}
ensureValidToken();
const QUrl url(m_apiBase + m_pathMonitoringLog);
QNetworkRequest request(url);
if (!m_accessToken.isEmpty()) {
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
}
QNetworkReply *reply = m_manager->post(request, multiPart);
multiPart->setParent(reply);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
if (!timeout.isActive()) {
qWarning() << "[NetworkService] postMonitoringLog timeout";
reply->abort();
} else if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[NetworkService] postMonitoringLog failed:" << reply->errorString()
<< "\n response body:" << reply->readAll();
}
reply->deleteLater();
}
void NetworkService::postMonitoringDump(const QString &text, const QByteArray &archive,
const QByteArray &image) {
qDebug() << "[NetworkService] postMonitoringDump enter — text.size=" << text.size()
<< "archive.size=" << archive.size() << "image.size=" << image.size();
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
addFormField(multiPart, "text", text);
if (!archive.isEmpty()) {
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
filePart.setHeader(
QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"file\"; filename=\"task_dump.zip\"")
);
filePart.setBody(archive);
multiPart->append(filePart);
}
if (!image.isEmpty()) {
QHttpPart imagePart;
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
imagePart.setHeader(
QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"image\"; filename=\"final_screen.png\"")
);
imagePart.setBody(image);
multiPart->append(imagePart);
}
ensureValidToken();
const QUrl url(m_apiBase + m_pathMonitoringDump);
QNetworkRequest request(url);
if (!m_accessToken.isEmpty()) {
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
}
qDebug() << "[NetworkService] postMonitoringDump POST" << url.toString();
QNetworkReply *reply = m_manager->post(request, multiPart);
multiPart->setParent(reply);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(120000);
loop.exec();
if (!timeout.isActive()) {
qWarning() << "[NetworkService] postMonitoringDump timeout";
reply->abort();
} else if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[NetworkService] postMonitoringDump failed:" << reply->errorString()
<< "\n response body:" << reply->readAll();
} else {
const QByteArray raw = reply->readAll();
const QJsonDocument doc = QJsonDocument::fromJson(raw);
if (doc.isObject() && !doc.object().value("success").toBool()) {
qWarning() << "[NetworkService] postMonitoringDump rejected:" << raw;
} else {
qDebug() << "[NetworkService] postMonitoringDump OK:" << raw;
}
}
reply->deleteLater();
}
void NetworkService::start() {
m_running = true;
// Проверяем токен при старте и планируем рефреш по времени жизни
ensureValidToken();
scheduleNextRefresh();
}
void NetworkService::deleteDeviceFromServer() {
if (!ensureValidToken()) { emit finished(); return; }
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
if (device.apiId.isEmpty()) {
qWarning() << "[deleteDeviceFromServer] no api_id for device" << m_deviceId;
// Удаляем только локально
DeviceDAO::deleteDevice(m_deviceId);
emit finished();
return;
}
const QJsonValue result = deleteJson(m_apiBase + m_pathDevice + device.apiId, true);
if (result.isUndefined()) {
qWarning() << "[deleteDeviceFromServer] failed to delete device on server:" << device.apiId;
} else {
qDebug() << "[deleteDeviceFromServer] deleted device on server:" << device.apiId;
}
DeviceDAO::deleteDevice(m_deviceId);
emit finished();
}
void NetworkService::disableAllProfilesOnServer() {
if (!ensureValidToken()) return;
const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true);
if (profileResult.isUndefined() || profileResult.isNull()) return;
const QJsonArray bankProfiles = profileResult.toObject()["bank_profiles"].toArray();
for (const QJsonValue &bpVal : bankProfiles) {
const QJsonObject bp = bpVal.toObject();
const QString bpId = bp["id"].toString();
const QString bpState = bp["state"].toString();
if (bpId.isEmpty() || bpState == "disabled") continue;
patchJson(m_apiBase + m_pathBankProfile + "/" + bpId + "/false", QJsonObject(), true);
qDebug() << "[shutdown] disabled bank_profile" << bpId;
// const QJsonArray materials = bp["materials"].toArray();
// for (const QJsonValue &matVal : materials) {
// const QJsonObject mat = matVal.toObject();
// const QString matId = mat["id"].toString();
// const QString matState = mat["state"].toString();
// if (matId.isEmpty() || matState == "disabled") continue;
//
// patchJson(m_apiBase + m_pathMaterial + "/" + matId + "/false", QJsonObject(), true);
// qDebug() << "[shutdown] disabled material" << matId;
// }
}
}
void NetworkService::stop() {
m_running = false;
QMetaObject::invokeMethod(this, [this]() {
disableAllProfilesOnServer();
emit finished();
}, Qt::QueuedConnection);
}