1393 lines
56 KiB
C++
1393 lines
56 KiB
C++
#include "NetworkService.h"
|
||
|
||
#include <QFile>
|
||
#include <QUrlQuery>
|
||
#include <QHttpPart>
|
||
#include <QNetworkReply>
|
||
#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"
|
||
|
||
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_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() << "Таймаут запроса:" << 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() << "Ошибка:" << reply->errorString() << path;
|
||
}
|
||
reply->deleteLater();
|
||
return result;
|
||
}
|
||
|
||
QJsonArray NetworkService::get(const QString &path, const QMap<QString, QString> ¶ms) {
|
||
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):" << 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;
|
||
if (!timeout.isActive()) {
|
||
qCritical() << "Таймаут запроса (postJson):" << path;
|
||
reply->abort();
|
||
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
||
"Timeout (POST): " + path, {}, reqBodyStr, {});
|
||
result = QJsonValue::Undefined;
|
||
} else if (reply->error() != QNetworkReply::NoError) {
|
||
const QByteArray responseBody = reply->readAll();
|
||
qCritical().noquote() << "Network error (POST):" << reply->errorString() << path
|
||
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
|
||
<< "\n response body:" << responseBody;
|
||
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
||
"Network error (POST): " + reply->errorString() + " " + path, {},
|
||
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): " + path, {},
|
||
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()) {
|
||
qCritical() << "Таймаут запроса (PATCH):" << path;
|
||
reply->abort();
|
||
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
||
"Timeout (PATCH): " + path, {}, reqBodyStr, {});
|
||
} else if (reply->error() != QNetworkReply::NoError) {
|
||
const QByteArray responseBody = reply->readAll();
|
||
qCritical().noquote() << "Network error (PATCH):" << reply->errorString() << path
|
||
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
|
||
<< "\n response body:" << responseBody;
|
||
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
||
"Network error (PATCH): " + reply->errorString() + " " + path, {},
|
||
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): " + 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):" << path;
|
||
reply->abort();
|
||
result = QJsonValue::Undefined;
|
||
} else if (reply->error() != QNetworkReply::NoError) {
|
||
qCritical().noquote() << "Network error (DELETE):" << reply->errorString() << 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):" << path;
|
||
reply->abort();
|
||
result = QJsonValue::Undefined;
|
||
} else if (reply->error() != QNetworkReply::NoError) {
|
||
qCritical().noquote() << "Network error (GET):" << reply->errorString() << 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) 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);
|
||
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);
|
||
|
||
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::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);
|
||
}
|