244 lines
11 KiB
C++
244 lines
11 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);
|
||
|
||
// Тело ответа сервера от последнего postJson/patchJson/getJson — пусто на
|
||
// успех. Используется скриптами, чтобы распознать сообщение об ошибке (напр.
|
||
// "already exists") и отдать UI осмысленный код.
|
||
QString lastResponseBody() const { return m_lastResponseBody; }
|
||
// Достаёт message/error/detail из JSON в lastResponseBody(). Если тело не
|
||
// JSON — возвращает его как есть; на пустом — пустую строку.
|
||
QString lastServerMessage() const;
|
||
|
||
public slots:
|
||
void postAccountsData();
|
||
|
||
void postBankProfile();
|
||
|
||
void patchBankProfile();
|
||
|
||
void toggleBankProfile();
|
||
|
||
void postMaterial();
|
||
|
||
void patchMaterial();
|
||
|
||
void toggleMaterial();
|
||
|
||
// DELETE /api/v1/profile/material/{material_id}. На успехе очищает
|
||
// локальный materialId у переданного account_id (если задан).
|
||
void deleteMaterial();
|
||
|
||
void postDeviceScreenshot();
|
||
|
||
void getPayments();
|
||
|
||
void postAppStatus();
|
||
|
||
void postEventStatus();
|
||
|
||
void insertTransactionData();
|
||
|
||
void updateTransactionData();
|
||
|
||
void loginAndSetup();
|
||
// Продолжение loginAndSetup после выбора десктопа пользователем.
|
||
// Пустой desktopId — выбран пункт «Создать новый». Вызывать через
|
||
// QueuedConnection: слот исполняется в потоке NetworkService.
|
||
void continueWithDesktop(const QString &desktopId);
|
||
// 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"));
|
||
|
||
// Отправляет JPEG-скриншот девайса на monitoringBase (/monitoring/screenshot,
|
||
// X-Monitoring-Token) для истории экрана. Q_INVOKABLE — вызывается через
|
||
// QueuedConnection из fire-and-forget потока DeviceScreener.
|
||
Q_INVOKABLE void postMonitoringScreenshot(const QString &desktopId,
|
||
const QString &deviceId,
|
||
const QString &name,
|
||
const QByteArray &jpeg);
|
||
|
||
void start();
|
||
void stop();
|
||
void disableAllProfilesOnServer();
|
||
void deleteDeviceFromServer();
|
||
|
||
signals:
|
||
void finished();
|
||
void finishedWithResult(int value);
|
||
// Эмитится из loginAndSetup, когда на сервере есть десктопы, но локального
|
||
// выбора нет — UI показывает окно выбора. После выбора UI вызывает
|
||
// continueWithDesktop(). До этого finished() НЕ эмитится (поток ждёт).
|
||
void desktopSelectionRequired(const QJsonArray &desktops);
|
||
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_pathMonitoringScreenshot;
|
||
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 = 15 * 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();
|
||
|
||
// POST /api/v1/desktop/ — создаёт новый десктоп, сохраняет desktop_id и
|
||
// profile_id в настройки. Возвращает id или пустую строку при ошибке.
|
||
QString createDesktopOnServer();
|
||
// Завершающая часть loginAndSetup: синхронизация устройств/профилей/статусов
|
||
// для выбранного desktopId и эмит finishedWithResult(1)+finished().
|
||
void finishSetupWithDesktop(const QString &desktopId);
|
||
|
||
// Не скрытые десктопы с сервера, сохранённые между loginAndSetup и
|
||
// continueWithDesktop (для поиска profile_id по выбранному id).
|
||
QJsonArray m_desktops;
|
||
|
||
public:
|
||
// Возвращает (лениво создавая) shared mutex для пары (deviceId, bankProfileId).
|
||
// Используется через QMutexLocker для сериализации PATCH-ов по одному профилю.
|
||
static QSharedPointer<QMutex> keyMutex(const QString &deviceId, const QString &bankProfileId);
|
||
};
|