Implemented `EventWindow` for viewing events with pagination and table-based UI. Added database migration to remove `UNIQUE` constraint on `external_id` in `events`. Enhanced DAO methods for event retrieval, pagination, and transaction updates. Integrated OCR recognition in transaction scripts and streamlined event handling in services.
718 lines
27 KiB
C++
718 lines
27 KiB
C++
#include "NetworkService.h"
|
||
|
||
#include <QUrlQuery>
|
||
#include <QHttpPart>
|
||
#include <QNetworkReply>
|
||
#include <QJsonObject>
|
||
#include <QJsonArray>
|
||
#include <QThread>
|
||
#include <QSettings>
|
||
#include <QTimer>
|
||
#include <QTimeZone>
|
||
|
||
#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_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_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<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;
|
||
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<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();
|
||
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<qint64>(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::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) {
|
||
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<qint64>(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<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() {
|
||
if (m_fetchingTasks) return;
|
||
m_fetchingTasks = true;
|
||
|
||
if (!ensureValidToken()) {
|
||
m_fetchingTasks = false;
|
||
return;
|
||
}
|
||
|
||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||
const QString accessToken = settings.value("common/access_token").toString();
|
||
|
||
QUrl url(m_apiBase + m_pathGetTasks);
|
||
QUrlQuery urlQuery;
|
||
const QStringList materialIds = AccountDAO::getActiveMaterialIds();
|
||
for (const QString &mid : materialIds) {
|
||
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.isNull() || 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);
|
||
addFormField(multiPart, "event", event);
|
||
addFormField(multiPart, "message", message);
|
||
|
||
QHttpPart imagePart;
|
||
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
|
||
imagePart.setHeader(
|
||
QNetworkRequest::ContentDispositionHeader,
|
||
QStringLiteral("form-data; name=\"image\"; filename=\"screenshot.png\"")
|
||
);
|
||
imagePart.setBody(image.isEmpty() ? QByteArray(1, '\0') : image);
|
||
multiPart->append(imagePart);
|
||
|
||
const QUrl url(m_apiBase + m_pathMonitoringLog);
|
||
QNetworkRequest request(url);
|
||
|
||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||
multiPart->setParent(reply);
|
||
|
||
connect(reply, &QNetworkReply::finished, reply, [reply]() {
|
||
if (reply->error() != QNetworkReply::NoError) {
|
||
qWarning() << "[NetworkService] postMonitoringLog failed:" << reply->errorString();
|
||
}
|
||
reply->deleteLater();
|
||
});
|
||
}
|
||
|
||
void NetworkService::start() {
|
||
m_running = true;
|
||
|
||
// Проверяем токен при старте и планируем рефреш по времени жизни
|
||
ensureValidToken();
|
||
scheduleNextRefresh();
|
||
}
|
||
|
||
void NetworkService::stop() {
|
||
m_running = false;
|
||
// Эмитируем finished() в потоке NetworkService, чтобы корректно завершить QThread
|
||
QMetaObject::invokeMethod(this, [this]() { emit finished(); }, Qt::QueuedConnection);
|
||
}
|