1
0
forked from BRT/arc
arc/services/net/NetworkService.h

256 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
#include <qhttpmultipart.h>
#include <QJsonArray>
#include <QNetworkAccessManager>
#include <QObject>
#include <QMap>
#include <QMutex>
#include <QSharedPointer>
#include <atomic>
#include <functional>
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();
// DELETE /api/v1/profile/bank_profile/{bank_profile_id}. Удаляет банковский
// профиль на сервере. bank_profile_id берётся из extra-данных.
void deleteBankProfileFromServer();
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);
// Результат postMonitoringDump (включая ручную отправку логов из меню).
// archiveFilename — ключ, по которому инициатор отличает свой аплоад (синглтон
// общий). ok=false ⇒ detail содержит причину (таймаут/сетевая/HTTP-ошибка).
void monitoringDumpFinished(bool ok, QString detail, QString archiveFilename);
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;
// buildMultiPart вызывается на КАЖДУЮ попытку (multipart потребляется за запрос).
// idempotent=false ⇒ ретраим только connect-phase сбои (без риска двойной отправки).
QJsonValue post(const std::function<QHttpMultiPart*()> &buildMultiPart,
const QString &path, bool idempotent);
QJsonArray get(const QString &path, const QMap<QString, QString> &params);
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);
};