204 lines
8.2 KiB
C++
204 lines
8.2 KiB
C++
#pragma once
|
||
#include <qhttpmultipart.h>
|
||
#include <QJsonArray>
|
||
#include <QNetworkAccessManager>
|
||
#include <QObject>
|
||
#include <QMap>
|
||
#include <QMutex>
|
||
#include <QSharedPointer>
|
||
#include <atomic>
|
||
|
||
class QWidget;
|
||
|
||
class NetworkService final : public QObject {
|
||
Q_OBJECT
|
||
|
||
public:
|
||
explicit NetworkService(QObject *parent = nullptr);
|
||
|
||
~NetworkService() override;
|
||
|
||
void setPayload(const QByteArray &json) { m_json = json; }
|
||
|
||
void setDeviceId(const QString &deviceId) {
|
||
m_deviceId = deviceId;
|
||
}
|
||
|
||
void addExtra(const QString &key, const QVariant &value);
|
||
|
||
// Подвешивает QMessageBox с текстом ошибки от backend (HTTP 409 — "материал
|
||
// заблокирован" и подобные) к ближайшему окну. Вызывать после `new NetworkService`
|
||
// в UI-коде, до запуска треда.
|
||
static void connectErrorDialog(QWidget *parent, NetworkService *net);
|
||
|
||
public slots:
|
||
void postAccountsData();
|
||
|
||
void postBankProfile();
|
||
|
||
void patchBankProfile();
|
||
|
||
void toggleBankProfile();
|
||
|
||
void postMaterial();
|
||
|
||
void patchMaterial();
|
||
|
||
void toggleMaterial();
|
||
|
||
void postDeviceScreenshot();
|
||
|
||
void getPayments();
|
||
|
||
void postAppStatus();
|
||
|
||
void postEventStatus();
|
||
|
||
void insertTransactionData();
|
||
|
||
void updateTransactionData();
|
||
|
||
void loginAndSetup();
|
||
// true — GET current_profile прошёл; false — таймаут/сеть. Используется
|
||
// health-счётчиком в runPeriodicSyncTick (правило #9).
|
||
bool syncCurrentProfile();
|
||
|
||
// POST /api/v1/profile/ping — heartbeat: даёт серверу понять, что десктоп
|
||
// онлайн. В теле уходят desktop_id и версия клиента. Вызывать периодически.
|
||
void pingProfile();
|
||
|
||
// Включает периодический ping (kPingProfileMs). Идемпотентен.
|
||
void startPeriodicPing();
|
||
|
||
// Включает периодическую синхронизацию статусов (60s). Должно быть вызвано
|
||
// ОДИН раз для долгоживущего инстанса после firstPassDone от DeviceScreener.
|
||
void startPeriodicSync();
|
||
|
||
// Обрабатывает переход устройства в офлайн (дебаунс-сигнал от DeviceScreener):
|
||
// выключает bank_profile локально и на сервере.
|
||
void onDeviceWentOffline(const QString &deviceId);
|
||
|
||
// Обрабатывает переход устройства в онлайн: пушит текущее локальное
|
||
// состояние профиля и материалов на сервер (локаль не меняем).
|
||
void onDeviceWentOnline(const QString &deviceId);
|
||
|
||
void createDevice();
|
||
|
||
void updateDevice();
|
||
|
||
void getTasks(const QStringList &materialIds = {});
|
||
|
||
void postTaskResult();
|
||
|
||
// toApiBase=true — POST на m_apiBase + /monitoring/log с Bearer-токеном
|
||
// (старый эндпоинт, используется для task_success). По умолчанию false —
|
||
// monitoringBase + X-Monitoring-Token (Telegram-релей, для ошибок).
|
||
void postMonitoringLog(const QString &type, const QString &event,
|
||
const QString &message, const QByteArray &image,
|
||
bool toApiBase = false);
|
||
|
||
void postMonitoringDump(const QString &text, const QByteArray &archive,
|
||
const QByteArray &image,
|
||
const QString &archiveFilename = QStringLiteral("task_dump.zip"));
|
||
|
||
void start();
|
||
void stop();
|
||
void disableAllProfilesOnServer();
|
||
void deleteDeviceFromServer();
|
||
|
||
signals:
|
||
void finished();
|
||
void finishedWithResult(int value);
|
||
void tokenRefreshFailed();
|
||
void tasksReceived(QJsonArray tasks);
|
||
// Эмитится при HTTP-ошибке PATCH-запроса (например, 409 'материал заблокирован').
|
||
// UI подписывается и показывает QMessageBox.
|
||
void apiPatchError(int httpStatus, QString message);
|
||
// healthy=false при kSyncFailureThreshold подряд неудачных тиков; healthy=true
|
||
// при первом успехе после такой серии. consecutiveFailures даёт текущий счёт.
|
||
void syncHealthChanged(bool healthy, int consecutiveFailures);
|
||
|
||
private:
|
||
QByteArray m_json;
|
||
QString m_deviceId;
|
||
QString m_token;
|
||
QString m_accessToken;
|
||
QString m_apiBase;
|
||
QMap<QString, QVariant> m_extraData = {};
|
||
bool m_running = true;
|
||
bool m_tokenErrorShown = false;
|
||
|
||
QString m_urlAccountUpdate;
|
||
QString m_urlPaymentsGet;
|
||
QString m_urlTransactionUpdate;
|
||
QString m_urlAppStatusUpdate;
|
||
QString m_urlTransactionInsert;
|
||
QString m_urlEventStatusUpdate;
|
||
QString m_pathAuthLogin;
|
||
QString m_pathAuthRefresh;
|
||
QString m_pathDesktopCreate;
|
||
QString m_pathDevice;
|
||
QString m_pathDeviceScreenshot;
|
||
QString m_pathBankProfile;
|
||
QString m_pathMaterial;
|
||
QString m_pathGetTasks;
|
||
QString m_pathTaskResult;
|
||
QString m_pathMonitoringLog;
|
||
QString m_pathMonitoringDump;
|
||
QString m_monitoringBase;
|
||
QString m_monitoringToken;
|
||
QString m_pathCurrentProfile;
|
||
QString m_pathProfileGetInfo;
|
||
QString m_pathProfilePing;
|
||
bool m_fetchingTasks = false;
|
||
|
||
// Последняя ошибка от postJson/patchJson/getJson — чтобы вызывающий код
|
||
// мог логировать тело ответа сервера, не дублируя парсинг ошибки.
|
||
QString m_lastErrorSummary;
|
||
QString m_lastResponseBody;
|
||
int m_lastHttpStatus = 0; // 0 если ответа не было (timeout); иначе HTTP-код
|
||
|
||
// Периодическая синхронизация
|
||
bool m_periodicSyncEnabled = false; // включается только после firstPassDone
|
||
std::atomic<bool> m_syncInProgress{false}; // защита от наложения тиков
|
||
int m_consecutiveSyncFailures = 0; // счётчик подряд неуспешных тиков
|
||
bool m_unhealthyEmitted = false; // чтобы не спамить syncHealthChanged
|
||
static constexpr int kSyncFailureThreshold = 3;
|
||
static constexpr int kPeriodicSyncMs = 60 * 1000;
|
||
static constexpr int kPingProfileMs = 5 * 1000;
|
||
|
||
// Heartbeat (ping)
|
||
bool m_pingEnabled = false;
|
||
std::atomic<bool> m_pingInProgress{false};
|
||
|
||
// Реестр мьютексов на пару (deviceId, bankProfileId) — общий для всех
|
||
// инстансов NetworkService. Используется для сериализации PATCH-операций.
|
||
static QMutex s_keyRegistryMutex;
|
||
static QMap<QString, QSharedPointer<QMutex>> s_keyMutexes;
|
||
|
||
QNetworkAccessManager *m_manager;
|
||
|
||
QJsonValue post(QHttpMultiPart *multiPart, const QString &path);
|
||
QJsonArray get(const QString &path, const QMap<QString, QString> ¶ms);
|
||
QJsonValue getJson(const QString &path, bool withAuth = false);
|
||
QJsonValue postJson(const QString &path, const QJsonObject &body, bool withAuth = false);
|
||
QJsonValue patchJson(const QString &path, const QJsonObject &body, bool withAuth = false);
|
||
QJsonValue deleteJson(const QString &path, bool withAuth = false);
|
||
|
||
bool refreshToken();
|
||
bool ensureValidToken();
|
||
void scheduleNextRefresh();
|
||
void scheduleNextStatusSync();
|
||
bool runPeriodicSyncTick(); // делает один тик, возвращает true при успехе
|
||
bool doPingProfile(); // тело ping без emit finished — для периодических тиков
|
||
void scheduleNextPing();
|
||
void syncDevicesFromServer(const QString &desktopId);
|
||
void syncBankProfilesFromServer();
|
||
void syncBanksIfEmpty();
|
||
|
||
public:
|
||
// Возвращает (лениво создавая) shared mutex для пары (deviceId, bankProfileId).
|
||
// Используется через QMutexLocker для сериализации PATCH-ов по одному профилю.
|
||
static QSharedPointer<QMutex> keyMutex(const QString &deviceId, const QString &bankProfileId);
|
||
};
|