#include "NetworkService.h" #include #include #include #include #include #include #include #include #include #include "dao/AccountDAO.h" #include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" #include "dao/EventDAO.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_manager = new QNetworkAccessManager(this); m_token = settings.value("common/token").toString(); m_accessToken = settings.value("common/access_token").toString(); } 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; connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); loop.exec(); // <- здесь и запускается event-loop потока QJsonValue result; 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; connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); loop.exec(); // <- здесь и запускается event-loop потока QJsonArray result; 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"].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(); AccountInfo account = AccountDAO::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; connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); loop.exec(); QJsonValue result; 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() { QSettings settings("config.ini", QSettings::IniFormat); const QString apiKey = settings.value("common/api_key").toString(); // Шаг 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(); settings.setValue("common/access_token", m_accessToken); settings.setValue("common/refresh_token", loginData["refresh_token"].toString()); const qint64 expiresIn = static_cast(loginData["expires_in"].toDouble()); settings.setValue("common/token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); // Шаг 2: проверяем есть ли desktop_id const QString desktopId = settings.value("common/desktop_id").toString(); if (!desktopId.isEmpty()) { emit finishedWithResult(1); emit finished(); return; } // Шаг 3: создаём desktop QJsonObject desktopBody; desktopBody["blue_token"] = QJsonValue::Null; desktopBody["name"] = QJsonValue::Null; desktopBody["contact"] = QJsonValue::Null; 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(); settings.setValue("common/desktop_id", desktopData["id"].toString()); settings.setValue("common/profile_id", desktopData["profile_id"].toString()); 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; connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); loop.exec(); QJsonValue result; 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; } 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) { ApplicationDAO::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::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) { AccountDAO::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; connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); loop.exec(); 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 QSettings settings("config.ini", QSettings::IniFormat); const QString desktopId = settings.value("common/desktop_id").toString(); 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() { QSettings settings("config.ini", QSettings::IniFormat); const QString storedRefreshToken = settings.value("common/refresh_token").toString(); 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(); settings.setValue("common/access_token", m_accessToken); settings.setValue("common/refresh_token", data["refresh_token"].toString()); const qint64 expiresIn = static_cast(data["expires_in"].toDouble()); settings.setValue("common/token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); m_tokenErrorShown = false; // сбрасываем если снова заработало qDebug() << "refreshToken: токен обновлён, истекает через" << expiresIn << "сек"; return true; } bool NetworkService::ensureValidToken() { const QSettings settings("config.ini", QSettings::IniFormat); const qint64 expiresAt = settings.value("common/token_expires_at", 0).toLongLong(); 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 QSettings settings("config.ini", QSettings::IniFormat); const qint64 expiresAt = settings.value("common/token_expires_at", 0).toLongLong(); 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::start() { m_running = true; // Проверяем токен при старте и планируем рефреш по времени жизни ensureValidToken(); scheduleNextRefresh(); // TODO: GET /api/v1/task/get_tasks (polling) } void NetworkService::stop() { m_running = false; // Эмитируем finished() в потоке NetworkService, чтобы корректно завершить QThread QMetaObject::invokeMethod(this, [this]() { emit finished(); }, Qt::QueuedConnection); }