#include "NetworkService.h" #include #include #include #include #include #include #include #include #include #include #include "dao/BankDAO.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 ¶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 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 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(); QJsonValue result; if (!timeout.isActive()) { qCritical() << "Таймаут запроса (postJson):" << path; reply->abort(); result = QJsonValue::Undefined; } else if (reply->error() != QNetworkReply::NoError) { const QByteArray responseBody = reply->readAll(); qCritical() << "Network error (POST):" << reply->errorString() << path << "\n response body:" << 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; 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(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: загрузка справочника банков если пустой syncBanksIfEmpty(); // Шаг 6: синхронизация статусов профилей/материалов с учётом онлайн-устройств syncCurrentProfile(); // Шаг 7: отправляем OFFLINE статус всех устройств (DeviceScreener обновит на ONLINE при обнаружении) { const QList 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(); QJsonValue result; if (!timeout.isActive()) { qCritical() << "Таймаут запроса (PATCH):" << path; reply->abort(); } else if (reply->error() != QNetworkReply::NoError) { const QByteArray responseBody = reply->readAll(); qCritical() << "Network error (PATCH):" << reply->errorString() << path << "\n response body:" << 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; } } } 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() << "Network error (DELETE):" << reply->errorString() << path; 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() << "Network error (GET):" << reply->errorString() << path; 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(); const QJsonArray bankProfiles = profileData["bank_profiles"].toArray(); // Маппинг apiId → локальный device_id const QList allDevices = DeviceDAO::getAllDevices(false); QMap 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(); const QString phone = bp["phone_number"].toString(); const QString firstName = bp["first_name"].toString(); const QString middleName = bp["middle_name"].toString(); const QString lastName = bp["last_name"].toString(); const QString currency = bp["currency"].isNull() ? "" : QString::number(bp["currency"].toInt()); 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 = false; 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 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 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(); QSet 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); // Проверяем: есть ли уже устройство с таким name в локальной БД 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 allDevices = DeviceDAO::getAllDevices(false); QSet 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 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::toggleBankProfile() { if (!ensureValidToken()) { 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 finished(); return; } const QString enableStr = enable ? "true" : "false"; const QString path = m_apiBase + m_pathBankProfile + "/" + bankProfileId + "/" + enableStr; patchJson(path, QJsonObject(), true); qDebug() << "[NetworkService] toggleBankProfile done:" << bankProfileId << "enable=" << enable; 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(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(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 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); } const QUrl url(m_apiBase + m_pathMonitoringLog); 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(&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); }