Remove outdated AppLogDAO, LogWindow, and related functionalities. Introduce NetworkErrorUtils for detailed error categorization and enhanced logging. Refactor EventHandler to support balance adjustments and optimize thread cleanup. Add retry and timeout mechanisms to network services with improved error handling.
This commit is contained in:
parent
36567e78a4
commit
82ac22f635
@ -77,6 +77,10 @@ void GetProfileInfoScript::doStart() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BankProfileDAO::updateAmount(app.id, parsedBalance);
|
BankProfileDAO::updateAmount(app.id, parsedBalance);
|
||||||
|
// У Душанбе все карты сидят на одном счёте, а экран деталей карты
|
||||||
|
// отдаёт placeholder "0,00 TJS" — единственный источник правды
|
||||||
|
// для materials.amount это баланс home-screen.
|
||||||
|
MaterialDAO::setAmountByApp(device.id, m_appCode, parsedBalance);
|
||||||
|
|
||||||
// Тапаем по кнопке меню (гамбургер, левый верхний угол)
|
// Тапаем по кнопке меню (гамбургер, левый верхний угол)
|
||||||
// Это первый clickable ImageView в верхней панели
|
// Это первый clickable ImageView в верхней панели
|
||||||
|
|||||||
@ -1,97 +0,0 @@
|
|||||||
#include "AppLogDAO.h"
|
|
||||||
|
|
||||||
#include <QSqlQuery>
|
|
||||||
#include <QSqlError>
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
#include "DatabaseManager.h"
|
|
||||||
#include "time/DateUtils.h"
|
|
||||||
|
|
||||||
bool AppLogDAO::insert(const AppLogEntry &entry) {
|
|
||||||
QSqlQuery q(DatabaseManager::instance().database());
|
|
||||||
q.prepare(R"(
|
|
||||||
INSERT INTO app_logs (source, device_id, message, screenshot, timestamp)
|
|
||||||
VALUES (:source, :device_id, :message, :screenshot, :timestamp)
|
|
||||||
)");
|
|
||||||
q.bindValue(":source", entry.source);
|
|
||||||
q.bindValue(":device_id", entry.deviceId);
|
|
||||||
q.bindValue(":message", entry.message);
|
|
||||||
q.bindValue(":screenshot", entry.screenshot.isEmpty()
|
|
||||||
? QVariant(QMetaType(QMetaType::QByteArray))
|
|
||||||
: QVariant(entry.screenshot));
|
|
||||||
q.bindValue(":timestamp", DateUtils::toUtcString(entry.timestamp));
|
|
||||||
if (!q.exec()) {
|
|
||||||
qWarning() << "[AppLogDAO] insert failed:" << q.lastError().text();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<AppLogEntry> AppLogDAO::getLogs(int page, int pageSize) {
|
|
||||||
QList<AppLogEntry> result;
|
|
||||||
QSqlQuery q(DatabaseManager::instance().database());
|
|
||||||
q.prepare(R"(
|
|
||||||
SELECT id, source, device_id, message, timestamp
|
|
||||||
FROM app_logs
|
|
||||||
ORDER BY id DESC
|
|
||||||
LIMIT :limit OFFSET :offset
|
|
||||||
)");
|
|
||||||
q.bindValue(":limit", pageSize);
|
|
||||||
q.bindValue(":offset", page * pageSize);
|
|
||||||
if (!q.exec()) {
|
|
||||||
qWarning() << "[AppLogDAO] getLogs failed:" << q.lastError().text();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
while (q.next()) {
|
|
||||||
AppLogEntry e;
|
|
||||||
e.id = q.value("id").toInt();
|
|
||||||
e.source = q.value("source").toString();
|
|
||||||
e.deviceId = q.value("device_id").toString();
|
|
||||||
e.message = q.value("message").toString();
|
|
||||||
e.timestamp = DateUtils::fromUtcString(q.value("timestamp").toString());
|
|
||||||
result.append(e);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int AppLogDAO::getTotalCount() {
|
|
||||||
QSqlQuery q(DatabaseManager::instance().database());
|
|
||||||
q.exec("SELECT COUNT(*) FROM app_logs");
|
|
||||||
if (q.next()) return q.value(0).toInt();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray AppLogDAO::getScreenshot(int id) {
|
|
||||||
QSqlQuery q(DatabaseManager::instance().database());
|
|
||||||
q.prepare("SELECT screenshot FROM app_logs WHERE id = :id");
|
|
||||||
q.bindValue(":id", id);
|
|
||||||
if (!q.exec() || !q.next()) return {};
|
|
||||||
return q.value(0).toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<AppLogEntry> AppLogDAO::getLogsWithScreenshots(int limit) {
|
|
||||||
QList<AppLogEntry> result;
|
|
||||||
QSqlQuery q(DatabaseManager::instance().database());
|
|
||||||
q.prepare(R"(
|
|
||||||
SELECT id, source, device_id, message, screenshot, timestamp
|
|
||||||
FROM app_logs
|
|
||||||
ORDER BY id DESC
|
|
||||||
LIMIT :limit
|
|
||||||
)");
|
|
||||||
q.bindValue(":limit", limit);
|
|
||||||
if (!q.exec()) {
|
|
||||||
qWarning() << "[AppLogDAO] getLogsWithScreenshots failed:" << q.lastError().text();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
while (q.next()) {
|
|
||||||
AppLogEntry e;
|
|
||||||
e.id = q.value("id").toInt();
|
|
||||||
e.source = q.value("source").toString();
|
|
||||||
e.deviceId = q.value("device_id").toString();
|
|
||||||
e.message = q.value("message").toString();
|
|
||||||
e.screenshot = q.value("screenshot").toByteArray();
|
|
||||||
e.timestamp = DateUtils::fromUtcString(q.value("timestamp").toString());
|
|
||||||
result.append(e);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QList>
|
|
||||||
#include "db/AppLogEntry.h"
|
|
||||||
|
|
||||||
class AppLogDAO {
|
|
||||||
public:
|
|
||||||
[[nodiscard]] static bool insert(const AppLogEntry &entry);
|
|
||||||
|
|
||||||
// Возвращает записи без BLOB-скриншота (лёгкая загрузка для таблицы)
|
|
||||||
[[nodiscard]] static QList<AppLogEntry> getLogs(int page, int pageSize);
|
|
||||||
|
|
||||||
[[nodiscard]] static int getTotalCount();
|
|
||||||
|
|
||||||
// Загружает скриншот отдельно по id (по клику в UI)
|
|
||||||
[[nodiscard]] static QByteArray getScreenshot(int id);
|
|
||||||
|
|
||||||
// Все записи со скриншотами (для экспорта)
|
|
||||||
[[nodiscard]] static QList<AppLogEntry> getLogsWithScreenshots(int limit = 100);
|
|
||||||
};
|
|
||||||
@ -44,25 +44,42 @@ BankInfo BankDAO::findByAlias(const QString &alias) {
|
|||||||
|
|
||||||
void BankDAO::replaceAll(const QList<BankInfo> &banks) {
|
void BankDAO::replaceAll(const QList<BankInfo> &banks) {
|
||||||
QSqlDatabase db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
|
|
||||||
|
if (!db.transaction()) {
|
||||||
|
qWarning() << "[BankDAO] replaceAll: cannot begin transaction:" << db.lastError().text();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
|
if (!query.exec("DELETE FROM banks")) {
|
||||||
|
qWarning() << "[BankDAO] replaceAll: DELETE failed:" << query.lastError().text();
|
||||||
|
db.rollback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
db.transaction();
|
if (!query.prepare(R"(
|
||||||
|
|
||||||
query.exec("DELETE FROM banks");
|
|
||||||
|
|
||||||
query.prepare(R"(
|
|
||||||
INSERT INTO banks (alias, name, nspk_name)
|
INSERT INTO banks (alias, name, nspk_name)
|
||||||
VALUES (:alias, :name, :nspk_name)
|
VALUES (:alias, :name, :nspk_name)
|
||||||
)");
|
)")) {
|
||||||
|
qWarning() << "[BankDAO] replaceAll: prepare INSERT failed:" << query.lastError().text();
|
||||||
|
db.rollback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const BankInfo &bank : banks) {
|
for (const BankInfo &bank : banks) {
|
||||||
query.bindValue(":alias", bank.alias);
|
query.bindValue(":alias", bank.alias);
|
||||||
query.bindValue(":name", bank.name);
|
query.bindValue(":name", bank.name);
|
||||||
query.bindValue(":nspk_name", bank.nspkName);
|
query.bindValue(":nspk_name", bank.nspkName);
|
||||||
if (!query.exec()) {
|
if (!query.exec()) {
|
||||||
qWarning() << "Ошибка при вставке bank:" << query.lastError().text();
|
qWarning() << "[BankDAO] replaceAll: INSERT failed for alias" << bank.alias
|
||||||
|
<< ":" << query.lastError().text() << "— rolling back";
|
||||||
|
db.rollback();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db.commit();
|
if (!db.commit()) {
|
||||||
|
qWarning() << "[BankDAO] replaceAll: commit failed:" << db.lastError().text();
|
||||||
|
db.rollback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -264,6 +264,18 @@ bool BankProfileDAO::updateAmount(const int appId, const double amount) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool BankProfileDAO::adjustAmount(const int appId, const double delta) {
|
||||||
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
query.prepare("UPDATE bank_profiles SET amount = amount - :delta WHERE id = :id");
|
||||||
|
query.bindValue(":delta", delta);
|
||||||
|
query.bindValue(":id", appId);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "[BankProfileDAO] adjustAmount failed:" << query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool BankProfileDAO::updateAppVersion(const int appId, const QString &appVersion) {
|
bool BankProfileDAO::updateAppVersion(const int appId, const QString &appVersion) {
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
|
|||||||
@ -36,6 +36,10 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] static bool updateAmount(int appId, double amount);
|
[[nodiscard]] static bool updateAmount(int appId, double amount);
|
||||||
|
|
||||||
|
// Атомарно вычитает delta из bank_profiles.amount.
|
||||||
|
// delta > 0 — расход, delta < 0 — пополнение.
|
||||||
|
[[nodiscard]] static bool adjustAmount(int appId, double delta);
|
||||||
|
|
||||||
[[nodiscard]] static bool updateAppVersion(int appId, const QString &appVersion);
|
[[nodiscard]] static bool updateAppVersion(int appId, const QString &appVersion);
|
||||||
|
|
||||||
[[nodiscard]] static BankProfileInfo getApplication(const QString &deviceId, const QString &appCode);
|
[[nodiscard]] static BankProfileInfo getApplication(const QString &deviceId, const QString &appCode);
|
||||||
|
|||||||
@ -128,6 +128,50 @@ bool MaterialDAO::updateMaterialId(const int accountId, const QString &materialI
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MaterialDAO::adjustAmount(const int id, const double delta) {
|
||||||
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
query.prepare("UPDATE materials SET amount = amount - :delta WHERE id = :id");
|
||||||
|
query.bindValue(":delta", delta);
|
||||||
|
query.bindValue(":id", id);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "[MaterialDAO] adjustAmount failed:" << query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaterialDAO::adjustAmountByApp(const QString &deviceId, const QString &appCode, const double delta) {
|
||||||
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
query.prepare(R"(
|
||||||
|
UPDATE materials SET amount = amount - :delta
|
||||||
|
WHERE device_id = :device_id AND app_code = :app_code
|
||||||
|
)");
|
||||||
|
query.bindValue(":delta", delta);
|
||||||
|
query.bindValue(":device_id", deviceId);
|
||||||
|
query.bindValue(":app_code", appCode);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "[MaterialDAO] adjustAmountByApp failed:" << query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaterialDAO::setAmountByApp(const QString &deviceId, const QString &appCode, const double amount) {
|
||||||
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
query.prepare(R"(
|
||||||
|
UPDATE materials SET amount = :amount
|
||||||
|
WHERE device_id = :device_id AND app_code = :app_code
|
||||||
|
)");
|
||||||
|
query.bindValue(":amount", amount);
|
||||||
|
query.bindValue(":device_id", deviceId);
|
||||||
|
query.bindValue(":app_code", appCode);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "[MaterialDAO] setAmountByApp failed:" << query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
MaterialInfo parseAccount(const QSqlQuery &query) {
|
MaterialInfo parseAccount(const QSqlQuery &query) {
|
||||||
MaterialInfo acc;
|
MaterialInfo acc;
|
||||||
acc.id = query.value("id").toInt();
|
acc.id = query.value("id").toInt();
|
||||||
|
|||||||
@ -26,6 +26,18 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] static bool updateMaterialId(int accountId, const QString &materialId);
|
[[nodiscard]] static bool updateMaterialId(int accountId, const QString &materialId);
|
||||||
|
|
||||||
|
// Атомарно вычитает delta из materials.amount для одного материала.
|
||||||
|
// delta > 0 — расход, delta < 0 — пополнение.
|
||||||
|
[[nodiscard]] static bool adjustAmount(int id, double delta);
|
||||||
|
|
||||||
|
// Атомарно вычитает delta из materials.amount для всех материалов
|
||||||
|
// в (deviceId, appCode) — для банков с одним счётом на все карты (Dushanbe).
|
||||||
|
[[nodiscard]] static bool adjustAmountByApp(const QString &deviceId, const QString &appCode, double delta);
|
||||||
|
|
||||||
|
// Перезаписывает materials.amount абсолютным значением для всех материалов
|
||||||
|
// в (deviceId, appCode). Используется после re-parse экрана.
|
||||||
|
[[nodiscard]] static bool setAmountByApp(const QString &deviceId, const QString &appCode, double amount);
|
||||||
|
|
||||||
[[nodiscard]] static QList<MaterialInfo> getAccounts(const QString &deviceId, const QString &appName);
|
[[nodiscard]] static QList<MaterialInfo> getAccounts(const QString &deviceId, const QString &appName);
|
||||||
|
|
||||||
[[nodiscard]] static MaterialInfo getAccountById(int accountId);
|
[[nodiscard]] static MaterialInfo getAccountById(int accountId);
|
||||||
|
|||||||
@ -102,3 +102,54 @@ pathTaskResult = /api/v1/task/task_result
|
|||||||
|
|
||||||
- **Bearer** — `Authorization: Bearer <access_token>` из `config.ini [common/access_token]`
|
- **Bearer** — `Authorization: Bearer <access_token>` из `config.ini [common/access_token]`
|
||||||
- **Legacy** — поле `token` в multipart-теле (из `config.ini [common/token]`)
|
- **Legacy** — поле `token` в multipart-теле (из `config.ini [common/token]`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Таймауты и ретраи
|
||||||
|
|
||||||
|
Каждый синхронный запрос крутится в локальном `QEventLoop` под охраной `QTimer`. Если таймер сработал раньше, чем `reply->finished` — это **client timeout**: запрос аборитится и повторяется заново.
|
||||||
|
|
||||||
|
### Параметры
|
||||||
|
|
||||||
|
| Параметр | Значение | Где |
|
||||||
|
|----------|----------|-----|
|
||||||
|
| Лимит ожидания | **30 секунд** | `kDefaultTimeoutMs` — все запросы |
|
||||||
|
| Лимит ожидания (dump) | **120 секунд** | `kDumpTimeoutMs` — только `postMonitoringDump` (большой zip-архив) |
|
||||||
|
| Макс. попыток | **3** | `kMaxRequestAttempts` — 1 основная + 2 повтора |
|
||||||
|
| Пауза между попытками | **0** | Ретрай идёт сразу после abort |
|
||||||
|
|
||||||
|
Константы и helper `awaitReply()` — в анонимном namespace в `services/net/NetworkService.cpp`.
|
||||||
|
|
||||||
|
### Когда ретраим
|
||||||
|
|
||||||
|
Ретрай срабатывает **только** при client timeout (наш `QTimer` сработал, reply не успел завершиться). На любых других исходах ретрая нет:
|
||||||
|
|
||||||
|
| Исход | Ретрай? | Почему |
|
||||||
|
|-------|---------|--------|
|
||||||
|
| Client timeout (наш QTimer) | ✅ да | сеть моргнула — стоит попробовать |
|
||||||
|
| HTTP 4xx (401/404/409/…) | ❌ нет | детерминированная ошибка, повтор не поможет |
|
||||||
|
| HTTP 5xx | ❌ нет | сервер ответил — это бизнес-сбой, не транспортный |
|
||||||
|
| `ConnectionRefused` / DNS / SSL | ❌ нет | повтор не починит инфраструктурную проблему |
|
||||||
|
| `success: false` в JSON | ❌ нет | прикладная ошибка |
|
||||||
|
|
||||||
|
После каждой неудачной попытки в лог пишется `Network timeout (POST) attempt N/3: <details>`; если все 3 попытки таймаутнулись — финальная строка `Timeout (POST) after 3 attempts: <details>` плюс запись в `GeneralLogDAO` (для `postJson`/`patchJson`).
|
||||||
|
|
||||||
|
### Покрытие методов
|
||||||
|
|
||||||
|
| Метод | Ретрай | Примечание |
|
||||||
|
|-------|--------|------------|
|
||||||
|
| `postJson` | ✅ | |
|
||||||
|
| `patchJson` | ✅ | |
|
||||||
|
| `getJson` | ✅ | |
|
||||||
|
| `deleteJson` | ✅ | |
|
||||||
|
| `get(path, params)` | ✅ | используется `getPayments()` (legacy) |
|
||||||
|
| `syncBanksIfEmpty()` инлайн-GET | ✅ | |
|
||||||
|
| `postDeviceScreenshot()` | ✅ | multipart пересобирается на каждой попытке |
|
||||||
|
| `postMonitoringLog()` | ✅ | multipart пересобирается на каждой попытке |
|
||||||
|
| `postMonitoringDump()` | ✅ | timeout 120s; multipart пересобирается |
|
||||||
|
| `post(QHttpMultiPart*, path)` | ❌ | multipart передаётся снаружи и удаляется вместе с `reply`; колл-сайты (`postEventStatus`/`insertTransactionData`/…) — legacy |
|
||||||
|
| `getTasks()` | ❌ | асинхронный (без `QEventLoop`); таймаут просто аборитит reply |
|
||||||
|
|
||||||
|
### Идемпотентность
|
||||||
|
|
||||||
|
⚠️ POST-запросы, создающие ресурсы (`/desktop/`, `/device/`, `/profile/bank_profile`, `/profile/material`), при ретрае могут создать дубль, если первая попытка дошла до сервера, но ответ потерялся. Сейчас защиты от этого нет — полагаемся на то, что 3 timeout-а подряд это редкая ситуация. Если станет проблемой — добавлять идемпотентные ключи на стороне бэкенда.
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <QString>
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QDateTime>
|
|
||||||
|
|
||||||
struct AppLogEntry {
|
|
||||||
int id = -1;
|
|
||||||
QString source; // "ozon/LoginAndCheck", "ozon/GetLastTransactions", etc.
|
|
||||||
QString deviceId; // ADB device id, может быть пустым
|
|
||||||
QString message; // текст ошибки
|
|
||||||
QByteArray screenshot; // PNG-байты скриншота (может быть пустым)
|
|
||||||
QDateTime timestamp;
|
|
||||||
};
|
|
||||||
@ -22,6 +22,7 @@
|
|||||||
#include "dao/BankProfileDAO.h"
|
#include "dao/BankProfileDAO.h"
|
||||||
#include "dao/MaterialDAO.h"
|
#include "dao/MaterialDAO.h"
|
||||||
#include "dao/SettingsDAO.h"
|
#include "dao/SettingsDAO.h"
|
||||||
|
#include "net/NetworkErrorUtils.h"
|
||||||
|
|
||||||
DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) {
|
DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) {
|
||||||
const QSettings settings("config.ini", QSettings::IniFormat);
|
const QSettings settings("config.ini", QSettings::IniFormat);
|
||||||
@ -131,7 +132,8 @@ QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body)
|
|||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
if (!timeout.isActive()) {
|
if (!timeout.isActive()) {
|
||||||
qCritical() << "DeviceScreener apiPost timeout:" << path;
|
qCritical().noquote() << "DeviceScreener apiPost timeout:"
|
||||||
|
<< NetworkErrorUtils::describeClientTimeout(reply, path);
|
||||||
reply->abort();
|
reply->abort();
|
||||||
} else if (reply->error() == QNetworkReply::NoError) {
|
} else if (reply->error() == QNetworkReply::NoError) {
|
||||||
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||||
@ -141,7 +143,8 @@ QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body)
|
|||||||
else qCritical() << "DeviceScreener apiPost error:" << root;
|
else qCritical() << "DeviceScreener apiPost error:" << root;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qCritical() << "DeviceScreener apiPost network error:" << reply->errorString();
|
qCritical().noquote() << "DeviceScreener apiPost error:"
|
||||||
|
<< NetworkErrorUtils::describeNetError(reply, path);
|
||||||
}
|
}
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
return result;
|
return result;
|
||||||
@ -166,10 +169,12 @@ void DeviceScreener::apiPatch(const QString &path, const QJsonObject &body) {
|
|||||||
loop.exec();
|
loop.exec();
|
||||||
|
|
||||||
if (!timeout.isActive()) {
|
if (!timeout.isActive()) {
|
||||||
qCritical() << "DeviceScreener apiPatch timeout:" << path;
|
qCritical().noquote() << "DeviceScreener apiPatch timeout:"
|
||||||
|
<< NetworkErrorUtils::describeClientTimeout(reply, path);
|
||||||
reply->abort();
|
reply->abort();
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
qCritical() << "DeviceScreener apiPatch network error:" << reply->errorString();
|
qCritical().noquote() << "DeviceScreener apiPatch error:"
|
||||||
|
<< NetworkErrorUtils::describeNetError(reply, path);
|
||||||
}
|
}
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
}
|
}
|
||||||
@ -225,10 +230,12 @@ void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray
|
|||||||
loop.exec();
|
loop.exec();
|
||||||
|
|
||||||
if (!timeout.isActive()) {
|
if (!timeout.isActive()) {
|
||||||
qWarning() << "postScreenshotToApi timeout for device" << apiId;
|
qWarning().noquote() << "postScreenshotToApi timeout:" << apiId
|
||||||
|
<< NetworkErrorUtils::describeClientTimeout(reply, path);
|
||||||
reply->abort();
|
reply->abort();
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
qWarning() << "postScreenshotToApi network error:" << reply->errorString();
|
qWarning().noquote() << "postScreenshotToApi error:" << apiId
|
||||||
|
<< NetworkErrorUtils::describeNetError(reply, path);
|
||||||
} else {
|
} else {
|
||||||
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||||
if (doc.isObject() && doc.object()["success"].toBool()) {
|
if (doc.isObject() && doc.object()["success"].toBool()) {
|
||||||
@ -278,7 +285,8 @@ QJsonValue DeviceScreener::apiGet(const QString &path) {
|
|||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
if (!timeout.isActive()) {
|
if (!timeout.isActive()) {
|
||||||
qCritical() << "DeviceScreener apiGet timeout:" << path;
|
qCritical().noquote() << "DeviceScreener apiGet timeout:"
|
||||||
|
<< NetworkErrorUtils::describeClientTimeout(reply, path);
|
||||||
reply->abort();
|
reply->abort();
|
||||||
} else if (reply->error() == QNetworkReply::NoError) {
|
} else if (reply->error() == QNetworkReply::NoError) {
|
||||||
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||||
@ -288,7 +296,8 @@ QJsonValue DeviceScreener::apiGet(const QString &path) {
|
|||||||
else qCritical() << "DeviceScreener apiGet error:" << root;
|
else qCritical() << "DeviceScreener apiGet error:" << root;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qCritical() << "DeviceScreener apiGet network error:" << reply->errorString();
|
qCritical().noquote() << "DeviceScreener apiGet error:"
|
||||||
|
<< NetworkErrorUtils::describeNetError(reply, path);
|
||||||
}
|
}
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data, int eventId = -1);
|
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data, int eventId = -1);
|
||||||
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description, const QByteArray &screenshot = {}, int eventId = -1);
|
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description, const QByteArray &screenshot = {}, int eventId = -1);
|
||||||
static void sendMonitoringLog(const QString &type, const QString &event, const QString &message, const QByteArray &image = {}, int eventId = -1);
|
static void sendMonitoringLog(const QString &type, const QString &event, const QString &message, const QByteArray &image = {}, int eventId = -1, bool toApiBase = false);
|
||||||
|
|
||||||
EventHandler::EventHandler(QObject *parent) : QObject(parent) {}
|
EventHandler::EventHandler(QObject *parent) : QObject(parent) {}
|
||||||
|
|
||||||
@ -228,14 +228,14 @@ static void sendTaskError(const QString &type, const QString &materialId, const
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void sendMonitoringLog(const QString &type, const QString &event, const QString &message,
|
static void sendMonitoringLog(const QString &type, const QString &event, const QString &message,
|
||||||
const QByteArray &image, int eventId) {
|
const QByteArray &image, int eventId, bool toApiBase) {
|
||||||
auto *thread = new QThread;
|
auto *thread = new QThread;
|
||||||
auto *networkService = new NetworkService;
|
auto *networkService = new NetworkService;
|
||||||
networkService->moveToThread(thread);
|
networkService->moveToThread(thread);
|
||||||
if (eventId > 0)
|
if (eventId > 0)
|
||||||
networkService->addExtra("internal_task_id", eventId);
|
networkService->addExtra("internal_task_id", eventId);
|
||||||
QObject::connect(thread, &QThread::started, networkService, [networkService, type, event, message, image]() {
|
QObject::connect(thread, &QThread::started, networkService, [networkService, type, event, message, image, toApiBase]() {
|
||||||
networkService->postMonitoringLog(type, event, message, image);
|
networkService->postMonitoringLog(type, event, message, image, toApiBase);
|
||||||
emit networkService->finished();
|
emit networkService->finished();
|
||||||
});
|
});
|
||||||
QObject::connect(networkService, &NetworkService::finished, thread, &QThread::quit);
|
QObject::connect(networkService, &NetworkService::finished, thread, &QThread::quit);
|
||||||
@ -430,21 +430,63 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
}
|
}
|
||||||
f.remove();
|
f.remove();
|
||||||
}
|
}
|
||||||
sendMonitoringLog("info", "task_success", msg, screenshot, event.id);
|
sendMonitoringLog("info", "task_success", msg, screenshot, event.id, /*toApiBase=*/true);
|
||||||
|
|
||||||
|
// Декрементируем материальный/профильный баланс на сумму расхода + комиссию.
|
||||||
|
// Между запусками FETCH_PROFILE это держит UI/локальный баланс
|
||||||
|
// в актуальном виде; drift по входящим всё равно сбросит следующий re-parse.
|
||||||
|
const MaterialInfo acc = MaterialDAO::getAccountById(event.accountId);
|
||||||
|
if (acc.id != -1) {
|
||||||
|
double fee = 0.0;
|
||||||
|
const QList<TransactionInfo> txList =
|
||||||
|
TransactionDAO::getTransactionsWithinHours(event.accountId, 1);
|
||||||
|
for (const auto &tx : txList) {
|
||||||
|
if (!tx.bankTrExternalId.isEmpty()
|
||||||
|
&& qAbs(tx.amount) == event.amount
|
||||||
|
&& tx.phone == event.phone) {
|
||||||
|
fee = tx.fee;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const double delta = event.amount + fee;
|
||||||
|
|
||||||
|
// Для Dushanbe все карты сидят на одном счёте — двигаем
|
||||||
|
// все материалы (см. memory: project_dushanbe_single_account).
|
||||||
|
if (acc.appCode == "dushanbe_city_bank") {
|
||||||
|
if (!MaterialDAO::adjustAmountByApp(acc.deviceId, acc.appCode, delta)) {
|
||||||
|
qWarning() << "[EventHandler] adjustAmountByApp failed for"
|
||||||
|
<< acc.deviceId << acc.appCode;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!MaterialDAO::adjustAmount(acc.id, delta)) {
|
||||||
|
qWarning() << "[EventHandler] adjustAmount failed for material" << acc.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BankProfileInfo app = BankProfileDAO::getApplication(acc.deviceId, acc.appCode);
|
||||||
|
if (app.id != -1) {
|
||||||
|
if (!BankProfileDAO::adjustAmount(app.id, delta)) {
|
||||||
|
qWarning() << "[EventHandler] BankProfileDAO::adjustAmount failed for app" << app.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qDebug() << "[EventHandler] balance decremented: material" << acc.id
|
||||||
|
<< "app" << acc.appCode << "delta=" << delta
|
||||||
|
<< "(amount=" << event.amount << "fee=" << fee << ")";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == EventType::FetchTransactions) {
|
if (event.type == EventType::FetchTransactions) {
|
||||||
const QString msg = QStringLiteral("[FETCH_TRANSACTIONS] bank=%1 material=%2")
|
const QString msg = QStringLiteral("[FETCH_TRANSACTIONS] bank=%1 material=%2")
|
||||||
.arg(event.bankName, event.externalId);
|
.arg(event.bankName, event.externalId);
|
||||||
const QByteArray scr = m_screenshots.take(key);
|
const QByteArray scr = m_screenshots.take(key);
|
||||||
sendMonitoringLog("info", "task_success", msg, scr, event.id);
|
sendMonitoringLog("info", "task_success", msg, scr, event.id, /*toApiBase=*/true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == EventType::FetchProfile) {
|
if (event.type == EventType::FetchProfile) {
|
||||||
const QString msg = QStringLiteral("[FETCH_PROFILE] bank=%1 material=%2")
|
const QString msg = QStringLiteral("[FETCH_PROFILE] bank=%1 material=%2")
|
||||||
.arg(event.bankName, event.externalId);
|
.arg(event.bankName, event.externalId);
|
||||||
const QByteArray scr = m_screenshots.take(key);
|
const QByteArray scr = m_screenshots.take(key);
|
||||||
sendMonitoringLog("info", "task_success", msg, scr, event.id);
|
sendMonitoringLog("info", "task_success", msg, scr, event.id, /*toApiBase=*/true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -496,15 +538,16 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
|||||||
// для поиска BankProfileInfo (чтобы PIN поднимался из правильной записи).
|
// для поиска BankProfileInfo (чтобы PIN поднимался из правильной записи).
|
||||||
const QString adbSerial = account.deviceId;
|
const QString adbSerial = account.deviceId;
|
||||||
|
|
||||||
|
// AutoConnection: worker emits из своего потока, лямбда выполняется в потоке
|
||||||
|
// EventHandler (где live `this`), там же где take()/clear() — m_screenshots
|
||||||
|
// не нужен mutex.
|
||||||
auto connectScreenshot = [this, key](Ozon::CommonScript *w) {
|
auto connectScreenshot = [this, key](Ozon::CommonScript *w) {
|
||||||
connect(w, &Ozon::CommonScript::screenshotReady, this,
|
connect(w, &Ozon::CommonScript::screenshotReady, this,
|
||||||
[this, key](const QByteArray &s) { m_screenshots[key] = s; },
|
[this, key](const QByteArray &s) { m_screenshots[key] = s; });
|
||||||
Qt::DirectConnection);
|
|
||||||
};
|
};
|
||||||
auto connectScreenshotDushanbe = [this, key](Dushanbe::CommonScript *w) {
|
auto connectScreenshotDushanbe = [this, key](Dushanbe::CommonScript *w) {
|
||||||
connect(w, &Dushanbe::CommonScript::screenshotReady, this,
|
connect(w, &Dushanbe::CommonScript::screenshotReady, this,
|
||||||
[this, key](const QByteArray &s) { m_screenshots[key] = s; },
|
[this, key](const QByteArray &s) { m_screenshots[key] = s; });
|
||||||
Qt::DirectConnection);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (event.type == EventType::FetchProfile) {
|
if (event.type == EventType::FetchProfile) {
|
||||||
@ -542,8 +585,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
|||||||
auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime, account.materialId);
|
auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime, account.materialId);
|
||||||
setup(w); connectScreenshot(w);
|
setup(w); connectScreenshot(w);
|
||||||
connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this,
|
connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this,
|
||||||
[this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; },
|
[this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; });
|
||||||
Qt::DirectConnection);
|
|
||||||
}
|
}
|
||||||
else if (appCode == "black")
|
else if (appCode == "black")
|
||||||
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
|
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
|
||||||
@ -551,8 +593,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
|||||||
auto *w = new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, searchAmount, searchTime, account.materialId);
|
auto *w = new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, searchAmount, searchTime, account.materialId);
|
||||||
setup(w); connectScreenshotDushanbe(w);
|
setup(w); connectScreenshotDushanbe(w);
|
||||||
connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this,
|
connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this,
|
||||||
[this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; },
|
[this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; });
|
||||||
Qt::DirectConnection);
|
|
||||||
}
|
}
|
||||||
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
||||||
}
|
}
|
||||||
@ -674,15 +715,22 @@ void EventHandler::processNextTask(const QString &deviceId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EventHandler::clearAll() {
|
void EventHandler::clearAll() {
|
||||||
|
// Параллельная остановка: сначала всем requestInterruption + quit,
|
||||||
|
// потом ждём каждый. terminate() намеренно НЕ используем — он опасен
|
||||||
|
// (heap corruption, утечки DB-локов); если поток не успел остановиться,
|
||||||
|
// abandon'им — ОС зачистит ресурсы при выходе процесса. Этот же подход
|
||||||
|
// применяет watchdog в startThreadForTask.
|
||||||
for (QThread *thread : m_threads) {
|
for (QThread *thread : m_threads) {
|
||||||
if (thread->isRunning()) {
|
if (thread->isRunning()) {
|
||||||
thread->requestInterruption();
|
thread->requestInterruption();
|
||||||
thread->quit();
|
thread->quit();
|
||||||
if (!thread->wait(3000)) {
|
}
|
||||||
qWarning() << "[EventHandler] Thread did not stop in 3s, terminating";
|
}
|
||||||
thread->terminate();
|
for (QThread *thread : m_threads) {
|
||||||
thread->wait(2000);
|
if (thread->isRunning() && !thread->wait(5000)) {
|
||||||
}
|
qWarning() << "[EventHandler] Thread did not stop in 5s on shutdown,"
|
||||||
|
<< "abandoning (no terminate to avoid heap corruption)";
|
||||||
|
continue; // не deleteLater для всё ещё работающего QThread
|
||||||
}
|
}
|
||||||
thread->deleteLater();
|
thread->deleteLater();
|
||||||
}
|
}
|
||||||
|
|||||||
80
services/net/NetworkErrorUtils.h
Normal file
80
services/net/NetworkErrorUtils.h
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// Хелперы для информативного логирования сетевых ошибок. Используются
|
||||||
|
// и в NetworkService, и в DeviceScreener — категория сразу показывает
|
||||||
|
// характер сбоя: timeout / refused / DNS / SSL / HTTP-уровень.
|
||||||
|
|
||||||
|
namespace NetworkErrorUtils {
|
||||||
|
|
||||||
|
// Удаляет схему+хост из URL, оставляет только путь (для логов/UI без секретов).
|
||||||
|
inline QString sanitizePath(const QString &fullUrl) {
|
||||||
|
const int schemeEnd = fullUrl.indexOf(QStringLiteral("://"));
|
||||||
|
if (schemeEnd < 0) return fullUrl;
|
||||||
|
const int pathStart = fullUrl.indexOf(QLatin1Char('/'), schemeEnd + 3);
|
||||||
|
return pathStart >= 0 ? fullUrl.mid(pathStart) : QStringLiteral("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Категория ошибки в виде короткого ярлыка. См. QNetworkReply::NetworkError.
|
||||||
|
inline QString networkErrorCategory(QNetworkReply::NetworkError err) {
|
||||||
|
switch (err) {
|
||||||
|
case QNetworkReply::NoError: return QStringLiteral("NO_ERROR");
|
||||||
|
case QNetworkReply::ConnectionRefusedError: return QStringLiteral("CONNECTION_REFUSED");
|
||||||
|
case QNetworkReply::RemoteHostClosedError: return QStringLiteral("REMOTE_CLOSED");
|
||||||
|
case QNetworkReply::HostNotFoundError: return QStringLiteral("DNS_FAIL");
|
||||||
|
case QNetworkReply::TimeoutError: return QStringLiteral("QT_TIMEOUT");
|
||||||
|
case QNetworkReply::OperationCanceledError: return QStringLiteral("CANCELLED");
|
||||||
|
case QNetworkReply::SslHandshakeFailedError: return QStringLiteral("SSL_HANDSHAKE");
|
||||||
|
case QNetworkReply::TemporaryNetworkFailureError: return QStringLiteral("TEMP_NET_FAIL");
|
||||||
|
case QNetworkReply::NetworkSessionFailedError: return QStringLiteral("NO_NETWORK_SESSION");
|
||||||
|
case QNetworkReply::BackgroundRequestNotAllowedError: return QStringLiteral("BG_NOT_ALLOWED");
|
||||||
|
case QNetworkReply::TooManyRedirectsError: return QStringLiteral("TOO_MANY_REDIRECTS");
|
||||||
|
case QNetworkReply::InsecureRedirectError: return QStringLiteral("INSECURE_REDIRECT");
|
||||||
|
case QNetworkReply::UnknownNetworkError: return QStringLiteral("UNKNOWN_NETWORK");
|
||||||
|
case QNetworkReply::ProxyConnectionRefusedError: return QStringLiteral("PROXY_REFUSED");
|
||||||
|
case QNetworkReply::ProxyConnectionClosedError: return QStringLiteral("PROXY_CLOSED");
|
||||||
|
case QNetworkReply::ProxyNotFoundError: return QStringLiteral("PROXY_NOT_FOUND");
|
||||||
|
case QNetworkReply::ProxyTimeoutError: return QStringLiteral("PROXY_TIMEOUT");
|
||||||
|
case QNetworkReply::ProxyAuthenticationRequiredError: return QStringLiteral("PROXY_AUTH_REQ");
|
||||||
|
case QNetworkReply::ContentAccessDenied: return QStringLiteral("HTTP_403");
|
||||||
|
case QNetworkReply::ContentOperationNotPermittedError:return QStringLiteral("OP_NOT_PERMITTED");
|
||||||
|
case QNetworkReply::ContentNotFoundError: return QStringLiteral("HTTP_404");
|
||||||
|
case QNetworkReply::AuthenticationRequiredError: return QStringLiteral("HTTP_401");
|
||||||
|
case QNetworkReply::ContentReSendError: return QStringLiteral("RESEND_REQUIRED");
|
||||||
|
case QNetworkReply::ContentConflictError: return QStringLiteral("HTTP_409");
|
||||||
|
case QNetworkReply::ContentGoneError: return QStringLiteral("HTTP_410");
|
||||||
|
case QNetworkReply::InternalServerError: return QStringLiteral("HTTP_500");
|
||||||
|
case QNetworkReply::OperationNotImplementedError: return QStringLiteral("HTTP_501");
|
||||||
|
case QNetworkReply::ServiceUnavailableError: return QStringLiteral("HTTP_503");
|
||||||
|
case QNetworkReply::UnknownServerError: return QStringLiteral("UNKNOWN_SERVER");
|
||||||
|
case QNetworkReply::ProtocolUnknownError: return QStringLiteral("PROTO_UNKNOWN");
|
||||||
|
case QNetworkReply::ProtocolInvalidOperationError: return QStringLiteral("PROTO_INVALID_OP");
|
||||||
|
case QNetworkReply::UnknownContentError: return QStringLiteral("UNKNOWN_CONTENT");
|
||||||
|
case QNetworkReply::ProtocolFailure: return QStringLiteral("PROTO_FAILURE");
|
||||||
|
default: return QStringLiteral("err_%1").arg(static_cast<int>(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Развёрнутое описание сетевой ошибки: HTTP-код + категория или просто
|
||||||
|
// категория + Qt errorString. Эндпоинт — без хоста.
|
||||||
|
inline QString describeNetError(QNetworkReply *reply, const QString &path) {
|
||||||
|
const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
const QString endpoint = sanitizePath(path);
|
||||||
|
const QString category = networkErrorCategory(reply->error());
|
||||||
|
if (httpCode > 0) {
|
||||||
|
return QStringLiteral("HTTP %1 [%2] %3").arg(httpCode).arg(category, endpoint);
|
||||||
|
}
|
||||||
|
return QStringLiteral("[%1] %2 (qt: %3)").arg(category, endpoint, reply->errorString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Случай нашего QTimer-таймаута (reply ещё не завершился). reply->error()
|
||||||
|
// обычно ещё NoError — помечаем категорию явно как CLIENT_TIMEOUT_30s.
|
||||||
|
inline QString describeClientTimeout(QNetworkReply *reply, const QString &path) {
|
||||||
|
const QString endpoint = sanitizePath(path);
|
||||||
|
const QString qtCategory = networkErrorCategory(reply->error());
|
||||||
|
return QStringLiteral("[CLIENT_TIMEOUT_30s] %1 (qt-state=%2 errorString=\"%3\")")
|
||||||
|
.arg(endpoint, qtCategory, reply->errorString());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace NetworkErrorUtils
|
||||||
@ -1,4 +1,5 @@
|
|||||||
#include "NetworkService.h"
|
#include "NetworkService.h"
|
||||||
|
#include "NetworkErrorUtils.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
@ -27,24 +28,34 @@
|
|||||||
#include "db/EventInfo.h"
|
#include "db/EventInfo.h"
|
||||||
|
|
||||||
// Убирает полный URL (хост:порт) из сообщений об ошибках, оставляя только
|
// Убирает полный URL (хост:порт) из сообщений об ошибках, оставляя только
|
||||||
// путь эндпоинта и HTTP-код. Нужно, чтобы адрес сервера не попадал в
|
// Хелперы для логирования сетевых ошибок вынесены в NetworkErrorUtils.h
|
||||||
// логи/UI/general_logs/application.log.
|
// чтобы переиспользовать их в DeviceScreener.
|
||||||
static QString sanitizePath(const QString &fullUrl) {
|
using NetworkErrorUtils::sanitizePath;
|
||||||
const int schemeEnd = fullUrl.indexOf("://");
|
using NetworkErrorUtils::describeNetError;
|
||||||
if (schemeEnd < 0) return fullUrl;
|
using NetworkErrorUtils::describeClientTimeout;
|
||||||
const int pathStart = fullUrl.indexOf('/', schemeEnd + 3);
|
|
||||||
return pathStart >= 0 ? fullUrl.mid(pathStart) : QStringLiteral("/");
|
namespace {
|
||||||
|
// Прокручивает event loop пока reply не завершится либо не сработает client timeout.
|
||||||
|
// true — reply finished (success или network error), false — сработал наш QTimer.
|
||||||
|
bool awaitReply(QNetworkReply *reply, int timeoutMs) {
|
||||||
|
QEventLoop loop;
|
||||||
|
QTimer timer;
|
||||||
|
timer.setSingleShot(true);
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||||
|
QObject::connect(reply, &QNetworkReply::errorOccurred, &loop,
|
||||||
|
[&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
||||||
|
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||||
|
timer.start(timeoutMs);
|
||||||
|
loop.exec();
|
||||||
|
return timer.isActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
static QString sanitizeNetError(QNetworkReply *reply, const QString &path) {
|
// На client timeout повторяем запрос ещё 2 раза (всего до 3 попыток).
|
||||||
const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
// На HTTP/network ошибках не ретраим — они детерминированы (401/409/refused/etc).
|
||||||
const QString endpoint = sanitizePath(path);
|
constexpr int kMaxRequestAttempts = 3;
|
||||||
if (httpCode > 0) {
|
constexpr int kDefaultTimeoutMs = 30000;
|
||||||
return QStringLiteral("HTTP %1 %2").arg(httpCode).arg(endpoint);
|
constexpr int kDumpTimeoutMs = 120000;
|
||||||
}
|
} // namespace
|
||||||
// Сетевой уровень (не HTTP) — выводим enum-код вместо строки с URL.
|
|
||||||
return QStringLiteral("net_err=%1 %2").arg(static_cast<int>(reply->error())).arg(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkService::NetworkService(QObject *parent) : QObject(parent) {
|
NetworkService::NetworkService(QObject *parent) : QObject(parent) {
|
||||||
const QSettings settings("config.ini", QSettings::IniFormat);
|
const QSettings settings("config.ini", QSettings::IniFormat);
|
||||||
@ -119,7 +130,7 @@ QJsonValue NetworkService::post(QHttpMultiPart *multiPart, const QString &path)
|
|||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
if (!timeout.isActive()) {
|
if (!timeout.isActive()) {
|
||||||
qCritical() << "Таймаут запроса:" << sanitizePath(path);
|
qCritical().noquote() << "Network timeout:" << describeClientTimeout(reply, path);
|
||||||
reply->abort();
|
reply->abort();
|
||||||
} else if (reply->error() == QNetworkReply::NoError) {
|
} else if (reply->error() == QNetworkReply::NoError) {
|
||||||
const QByteArray raw = reply->readAll();
|
const QByteArray raw = reply->readAll();
|
||||||
@ -134,7 +145,7 @@ QJsonValue NetworkService::post(QHttpMultiPart *multiPart, const QString &path)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qCritical() << "Ошибка:" << sanitizeNetError(reply, path);
|
qCritical() << "Ошибка:" << describeNetError(reply, path);
|
||||||
}
|
}
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
return result;
|
return result;
|
||||||
@ -149,24 +160,24 @@ QJsonArray NetworkService::get(const QString &path, const QMap<QString, QString>
|
|||||||
url.setQuery(query);
|
url.setQuery(query);
|
||||||
|
|
||||||
const QNetworkRequest request(url);
|
const QNetworkRequest request(url);
|
||||||
QNetworkReply *reply = m_manager->get(request);
|
|
||||||
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> guard(reply);
|
|
||||||
|
|
||||||
QEventLoop loop;
|
QNetworkReply *reply = nullptr;
|
||||||
QTimer timeout;
|
bool timedOut = true;
|
||||||
timeout.setSingleShot(true);
|
QString lastTimeoutDetail;
|
||||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
|
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||||
loop.quit();
|
reply = m_manager->get(request);
|
||||||
});
|
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
lastTimeoutDetail = describeClientTimeout(reply, path);
|
||||||
timeout.start(30000);
|
qCritical().noquote() << "Network timeout (GET) attempt" << attempt
|
||||||
loop.exec();
|
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
||||||
|
reply->abort();
|
||||||
|
}
|
||||||
|
|
||||||
QJsonArray result;
|
QJsonArray result;
|
||||||
if (!timeout.isActive()) {
|
if (timedOut) {
|
||||||
qCritical() << "Таймаут запроса (GET):" << sanitizePath(path);
|
qCritical().noquote() << "Network timeout (GET) after"
|
||||||
reply->abort();
|
<< kMaxRequestAttempts << "attempts:" << lastTimeoutDetail;
|
||||||
} else if (reply->error() == QNetworkReply::NoError) {
|
} else if (reply->error() == QNetworkReply::NoError) {
|
||||||
const QByteArray raw = reply->readAll();
|
const QByteArray raw = reply->readAll();
|
||||||
QJsonParseError err;
|
QJsonParseError err;
|
||||||
@ -250,30 +261,32 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
|
|||||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson());
|
const QByteArray bodyBytes = QJsonDocument(body).toJson();
|
||||||
QEventLoop loop;
|
|
||||||
QTimer timeout;
|
|
||||||
timeout.setSingleShot(true);
|
|
||||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
|
||||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
|
|
||||||
loop.quit();
|
|
||||||
});
|
|
||||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
|
||||||
timeout.start(30000);
|
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
||||||
QJsonValue result;
|
|
||||||
const QString shortPath = sanitizePath(path);
|
const QString shortPath = sanitizePath(path);
|
||||||
if (!timeout.isActive()) {
|
|
||||||
qCritical() << "Таймаут запроса (postJson):" << shortPath;
|
QNetworkReply *reply = nullptr;
|
||||||
|
bool timedOut = true;
|
||||||
|
QString lastTimeoutDetail;
|
||||||
|
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||||
|
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||||
|
reply = m_manager->post(request, bodyBytes);
|
||||||
|
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||||
|
lastTimeoutDetail = describeClientTimeout(reply, path);
|
||||||
|
qCritical().noquote() << "Network timeout (POST) attempt" << attempt
|
||||||
|
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
||||||
reply->abort();
|
reply->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonValue result;
|
||||||
|
if (timedOut) {
|
||||||
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
||||||
"Timeout (POST): " + shortPath, {}, reqBodyStr, {});
|
"Timeout (POST) after " + QString::number(kMaxRequestAttempts)
|
||||||
|
+ " attempts: " + lastTimeoutDetail, {}, reqBodyStr, {});
|
||||||
result = QJsonValue::Undefined;
|
result = QJsonValue::Undefined;
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
const QByteArray responseBody = reply->readAll();
|
const QByteArray responseBody = reply->readAll();
|
||||||
const QString errSummary = sanitizeNetError(reply, path);
|
const QString errSummary = describeNetError(reply, path);
|
||||||
qCritical().noquote() << "Network error (POST):" << errSummary
|
qCritical().noquote() << "Network error (POST):" << errSummary
|
||||||
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
|
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
|
||||||
<< "\n response body:" << responseBody;
|
<< "\n response body:" << responseBody;
|
||||||
@ -482,28 +495,31 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod
|
|||||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson());
|
const QByteArray bodyBytes = QJsonDocument(body).toJson();
|
||||||
QEventLoop loop;
|
|
||||||
QTimer timeout;
|
|
||||||
timeout.setSingleShot(true);
|
|
||||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
|
||||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
|
||||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
|
||||||
timeout.start(30000);
|
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
||||||
QJsonValue result;
|
|
||||||
if (!timeout.isActive()) {
|
QNetworkReply *reply = nullptr;
|
||||||
const QString shortPath = sanitizePath(path);
|
bool timedOut = true;
|
||||||
qCritical() << "Таймаут запроса (PATCH):" << shortPath;
|
QString lastTimeoutDetail;
|
||||||
|
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||||
|
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||||
|
reply = m_manager->sendCustomRequest(request, "PATCH", bodyBytes);
|
||||||
|
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||||
|
lastTimeoutDetail = describeClientTimeout(reply, path);
|
||||||
|
qCritical().noquote() << "Network timeout (PATCH) attempt" << attempt
|
||||||
|
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
||||||
reply->abort();
|
reply->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonValue result;
|
||||||
|
if (timedOut) {
|
||||||
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
GeneralLogDAO::insert("CRITICAL", "NetworkService",
|
||||||
"Timeout (PATCH): " + shortPath, {}, reqBodyStr, {});
|
"Timeout (PATCH) after " + QString::number(kMaxRequestAttempts)
|
||||||
|
+ " attempts: " + lastTimeoutDetail, {}, reqBodyStr, {});
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
const QByteArray responseBody = reply->readAll();
|
const QByteArray responseBody = reply->readAll();
|
||||||
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
const QString errSummary = sanitizeNetError(reply, path);
|
const QString errSummary = describeNetError(reply, path);
|
||||||
qCritical().noquote() << "Network error (PATCH):" << errSummary
|
qCritical().noquote() << "Network error (PATCH):" << errSummary
|
||||||
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
|
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId)
|
||||||
<< "\n response body:" << responseBody;
|
<< "\n response body:" << responseBody;
|
||||||
@ -567,23 +583,26 @@ QJsonValue NetworkService::deleteJson(const QString &path, bool withAuth) {
|
|||||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = m_manager->deleteResource(request);
|
QNetworkReply *reply = nullptr;
|
||||||
QEventLoop loop;
|
bool timedOut = true;
|
||||||
QTimer timeout;
|
QString lastTimeoutDetail;
|
||||||
timeout.setSingleShot(true);
|
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
reply = m_manager->deleteResource(request);
|
||||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||||
timeout.start(30000);
|
lastTimeoutDetail = describeClientTimeout(reply, path);
|
||||||
loop.exec();
|
qCritical().noquote() << "Network timeout (DELETE) attempt" << attempt
|
||||||
|
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
||||||
|
reply->abort();
|
||||||
|
}
|
||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
if (!timeout.isActive()) {
|
if (timedOut) {
|
||||||
qCritical() << "Таймаут запроса (DELETE):" << sanitizePath(path);
|
qCritical().noquote() << "Network timeout (DELETE) after"
|
||||||
reply->abort();
|
<< kMaxRequestAttempts << "attempts:" << lastTimeoutDetail;
|
||||||
result = QJsonValue::Undefined;
|
result = QJsonValue::Undefined;
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
qCritical().noquote() << "Network error (DELETE):" << sanitizeNetError(reply, path)
|
qCritical().noquote() << "Network error (DELETE):" << describeNetError(reply, path)
|
||||||
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId);
|
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId);
|
||||||
result = QJsonValue::Undefined;
|
result = QJsonValue::Undefined;
|
||||||
} else {
|
} else {
|
||||||
@ -611,25 +630,26 @@ QJsonValue NetworkService::getJson(const QString &path, bool withAuth) {
|
|||||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = m_manager->get(request);
|
QNetworkReply *reply = nullptr;
|
||||||
QEventLoop loop;
|
bool timedOut = true;
|
||||||
QTimer timeout;
|
QString lastTimeoutDetail;
|
||||||
timeout.setSingleShot(true);
|
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
|
reply = m_manager->get(request);
|
||||||
loop.quit();
|
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||||
});
|
lastTimeoutDetail = describeClientTimeout(reply, path);
|
||||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
qCritical().noquote() << "Network timeout (getJson) attempt" << attempt
|
||||||
timeout.start(30000);
|
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
||||||
loop.exec();
|
reply->abort();
|
||||||
|
}
|
||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
if (!timeout.isActive()) {
|
if (timedOut) {
|
||||||
qCritical() << "Таймаут запроса (getJson):" << sanitizePath(path);
|
qCritical().noquote() << "Network timeout (getJson) after"
|
||||||
reply->abort();
|
<< kMaxRequestAttempts << "attempts:" << lastTimeoutDetail;
|
||||||
result = QJsonValue::Undefined;
|
result = QJsonValue::Undefined;
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
qCritical().noquote() << "Network error (GET):" << sanitizeNetError(reply, path)
|
qCritical().noquote() << "Network error (GET):" << describeNetError(reply, path)
|
||||||
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId);
|
<< (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId);
|
||||||
result = QJsonValue::Undefined;
|
result = QJsonValue::Undefined;
|
||||||
} else {
|
} else {
|
||||||
@ -803,20 +823,22 @@ void NetworkService::syncBanksIfEmpty() {
|
|||||||
|
|
||||||
qDebug() << "[syncBanksIfEmpty] banks table empty, downloading...";
|
qDebug() << "[syncBanksIfEmpty] banks table empty, downloading...";
|
||||||
|
|
||||||
QNetworkRequest request(QUrl("https://api.brtservice.io/api/rest/info/"));
|
const QString path = QStringLiteral("https://api.brtservice.io/api/rest/info/");
|
||||||
QNetworkReply *reply = m_manager->get(request);
|
QNetworkRequest request{QUrl(path)};
|
||||||
QEventLoop loop;
|
|
||||||
QTimer timeout;
|
|
||||||
timeout.setSingleShot(true);
|
|
||||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
|
||||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
|
||||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
|
||||||
timeout.start(30000);
|
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
if (!timeout.isActive()) {
|
QNetworkReply *reply = nullptr;
|
||||||
qWarning() << "[syncBanksIfEmpty] timeout";
|
bool timedOut = true;
|
||||||
|
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||||
|
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||||
|
reply = m_manager->get(request);
|
||||||
|
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||||
|
qWarning().noquote() << "[syncBanksIfEmpty] timeout attempt" << attempt
|
||||||
|
<< "/" << kMaxRequestAttempts;
|
||||||
reply->abort();
|
reply->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timedOut) {
|
||||||
|
qWarning() << "[syncBanksIfEmpty] timeout after" << kMaxRequestAttempts << "attempts";
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -920,7 +942,7 @@ void NetworkService::syncDevicesFromServer(const QString &desktopId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::syncCurrentProfile() {
|
bool NetworkService::syncCurrentProfile() {
|
||||||
// Локаль — источник истины. Логика: GET current_profile, построить
|
// Локаль — источник истины. Логика: GET current_profile, построить
|
||||||
// map id→state, пройти по локальным bank_profiles/materials, для
|
// map id→state, пройти по локальным bank_profiles/materials, для
|
||||||
// каждого расхождения отправить PATCH с локальным значением.
|
// каждого расхождения отправить PATCH с локальным значением.
|
||||||
@ -928,7 +950,7 @@ void NetworkService::syncCurrentProfile() {
|
|||||||
const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true);
|
const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true);
|
||||||
if (profileResult.isUndefined() || profileResult.isNull()) {
|
if (profileResult.isUndefined() || profileResult.isNull()) {
|
||||||
qWarning() << "[syncCurrentProfile] GET current_profile failed";
|
qWarning() << "[syncCurrentProfile] GET current_profile failed";
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QJsonObject profileData = profileResult.toObject();
|
const QJsonObject profileData = profileResult.toObject();
|
||||||
@ -1009,6 +1031,7 @@ void NetworkService::syncCurrentProfile() {
|
|||||||
|
|
||||||
qDebug() << "[syncCurrentProfile] done — pushed" << pushedProfiles << "profiles,"
|
qDebug() << "[syncCurrentProfile] done — pushed" << pushedProfiles << "profiles,"
|
||||||
<< pushedMaterials << "materials";
|
<< pushedMaterials << "materials";
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::postBankProfile() {
|
void NetworkService::postBankProfile() {
|
||||||
@ -1227,37 +1250,37 @@ void NetworkService::postDeviceScreenshot() {
|
|||||||
return;
|
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);
|
const QUrl url(m_apiBase + m_pathDeviceScreenshot + device.apiId);
|
||||||
QNetworkRequest request(url);
|
QNetworkRequest request(url);
|
||||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||||
|
|
||||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
QNetworkReply *reply = nullptr;
|
||||||
multiPart->setParent(reply);
|
bool timedOut = true;
|
||||||
|
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||||
|
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||||
|
|
||||||
QEventLoop loop;
|
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
QTimer timeout;
|
QHttpPart filePart;
|
||||||
timeout.setSingleShot(true);
|
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
|
||||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
filePart.setHeader(
|
||||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
|
QNetworkRequest::ContentDispositionHeader,
|
||||||
loop.quit();
|
QStringLiteral("form-data; name=\"file\"; filename=\"%1.jpg\"").arg(m_deviceId)
|
||||||
});
|
);
|
||||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
filePart.setBody(m_json);
|
||||||
timeout.start(30000);
|
multiPart->append(filePart);
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
if (!timeout.isActive()) {
|
reply = m_manager->post(request, multiPart);
|
||||||
qWarning() << "[NetworkService] postDeviceScreenshot timeout for device" << device.apiId;
|
multiPart->setParent(reply);
|
||||||
|
|
||||||
|
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||||
|
qWarning() << "[NetworkService] postDeviceScreenshot timeout attempt"
|
||||||
|
<< attempt << "/" << kMaxRequestAttempts << "for device" << device.apiId;
|
||||||
reply->abort();
|
reply->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timedOut) {
|
||||||
|
qWarning() << "[NetworkService] postDeviceScreenshot timeout after"
|
||||||
|
<< kMaxRequestAttempts << "attempts for device" << device.apiId;
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
qWarning() << "[NetworkService] postDeviceScreenshot failed:" << reply->errorString();
|
qWarning() << "[NetworkService] postDeviceScreenshot failed:" << reply->errorString();
|
||||||
} else {
|
} else {
|
||||||
@ -1455,49 +1478,59 @@ void NetworkService::postTaskResult() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::postMonitoringLog(const QString &type, const QString &event,
|
void NetworkService::postMonitoringLog(const QString &type, const QString &event,
|
||||||
const QString &message, const QByteArray &image) {
|
const QString &message, const QByteArray &image,
|
||||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
bool toApiBase) {
|
||||||
|
QNetworkRequest request;
|
||||||
addFormField(multiPart, "type", type.toUpper());
|
if (toApiBase) {
|
||||||
addFormField(multiPart, "event", event);
|
ensureValidToken();
|
||||||
addFormField(multiPart, "message", message);
|
request.setUrl(QUrl(m_apiBase + m_pathMonitoringLog));
|
||||||
|
if (!m_accessToken.isEmpty()) {
|
||||||
|
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
request.setUrl(QUrl(m_monitoringBase + m_pathMonitoringLog));
|
||||||
|
if (!m_monitoringToken.isEmpty()) {
|
||||||
|
request.setRawHeader("X-Monitoring-Token", m_monitoringToken.toUtf8());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const int internalTaskId = m_extraData.value("internal_task_id").toInt();
|
const int internalTaskId = m_extraData.value("internal_task_id").toInt();
|
||||||
if (internalTaskId > 0) {
|
|
||||||
addFormField(multiPart, "internal_task_id", QString::number(internalTaskId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!image.isEmpty()) {
|
QNetworkReply *reply = nullptr;
|
||||||
QHttpPart imagePart;
|
bool timedOut = true;
|
||||||
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
|
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||||
imagePart.setHeader(
|
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||||
QNetworkRequest::ContentDispositionHeader,
|
|
||||||
QStringLiteral("form-data; name=\"image\"; filename=\"screenshot.png\"")
|
|
||||||
);
|
|
||||||
imagePart.setBody(image);
|
|
||||||
multiPart->append(imagePart);
|
|
||||||
}
|
|
||||||
|
|
||||||
const QUrl url(m_monitoringBase + m_pathMonitoringLog);
|
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
QNetworkRequest request(url);
|
addFormField(multiPart, "type", type.toUpper());
|
||||||
if (!m_monitoringToken.isEmpty()) {
|
addFormField(multiPart, "event", event);
|
||||||
request.setRawHeader("X-Monitoring-Token", m_monitoringToken.toUtf8());
|
addFormField(multiPart, "message", message);
|
||||||
}
|
if (internalTaskId > 0) {
|
||||||
|
addFormField(multiPart, "internal_task_id", QString::number(internalTaskId));
|
||||||
|
}
|
||||||
|
if (!image.isEmpty()) {
|
||||||
|
QHttpPart imagePart;
|
||||||
|
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
|
||||||
|
imagePart.setHeader(
|
||||||
|
QNetworkRequest::ContentDispositionHeader,
|
||||||
|
QStringLiteral("form-data; name=\"image\"; filename=\"screenshot.png\"")
|
||||||
|
);
|
||||||
|
imagePart.setBody(image);
|
||||||
|
multiPart->append(imagePart);
|
||||||
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
reply = m_manager->post(request, multiPart);
|
||||||
multiPart->setParent(reply);
|
multiPart->setParent(reply);
|
||||||
|
|
||||||
QEventLoop loop;
|
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
||||||
QTimer timeout;
|
qWarning() << "[NetworkService] postMonitoringLog timeout attempt"
|
||||||
timeout.setSingleShot(true);
|
<< attempt << "/" << kMaxRequestAttempts;
|
||||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
|
||||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
|
||||||
timeout.start(30000);
|
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
if (!timeout.isActive()) {
|
|
||||||
qWarning() << "[NetworkService] postMonitoringLog timeout";
|
|
||||||
reply->abort();
|
reply->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timedOut) {
|
||||||
|
qWarning() << "[NetworkService] postMonitoringLog timeout after"
|
||||||
|
<< kMaxRequestAttempts << "attempts";
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
qWarning() << "[NetworkService] postMonitoringLog failed:" << reply->errorString()
|
qWarning() << "[NetworkService] postMonitoringLog failed:" << reply->errorString()
|
||||||
<< "\n response body:" << reply->readAll();
|
<< "\n response body:" << reply->readAll();
|
||||||
@ -1511,31 +1544,6 @@ void NetworkService::postMonitoringDump(const QString &text, const QByteArray &a
|
|||||||
qDebug() << "[NetworkService] postMonitoringDump enter — text.size=" << text.size()
|
qDebug() << "[NetworkService] postMonitoringDump enter — text.size=" << text.size()
|
||||||
<< "archive.size=" << archive.size() << "image.size=" << image.size()
|
<< "archive.size=" << archive.size() << "image.size=" << image.size()
|
||||||
<< "archiveFilename=" << archiveFilename;
|
<< "archiveFilename=" << archiveFilename;
|
||||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
|
||||||
|
|
||||||
addFormField(multiPart, "text", text);
|
|
||||||
|
|
||||||
if (!archive.isEmpty()) {
|
|
||||||
QHttpPart filePart;
|
|
||||||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
|
|
||||||
filePart.setHeader(
|
|
||||||
QNetworkRequest::ContentDispositionHeader,
|
|
||||||
QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(archiveFilename)
|
|
||||||
);
|
|
||||||
filePart.setBody(archive);
|
|
||||||
multiPart->append(filePart);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!image.isEmpty()) {
|
|
||||||
QHttpPart imagePart;
|
|
||||||
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
|
|
||||||
imagePart.setHeader(
|
|
||||||
QNetworkRequest::ContentDispositionHeader,
|
|
||||||
QStringLiteral("form-data; name=\"image\"; filename=\"final_screen.png\"")
|
|
||||||
);
|
|
||||||
imagePart.setBody(image);
|
|
||||||
multiPart->append(imagePart);
|
|
||||||
}
|
|
||||||
|
|
||||||
const QUrl url(m_monitoringBase + m_pathMonitoringDump);
|
const QUrl url(m_monitoringBase + m_pathMonitoringDump);
|
||||||
QNetworkRequest request(url);
|
QNetworkRequest request(url);
|
||||||
@ -1544,20 +1552,47 @@ void NetworkService::postMonitoringDump(const QString &text, const QByteArray &a
|
|||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "[NetworkService] postMonitoringDump POST" << url.toString();
|
qDebug() << "[NetworkService] postMonitoringDump POST" << url.toString();
|
||||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
|
||||||
multiPart->setParent(reply);
|
|
||||||
|
|
||||||
QEventLoop loop;
|
QNetworkReply *reply = nullptr;
|
||||||
QTimer timeout;
|
bool timedOut = true;
|
||||||
timeout.setSingleShot(true);
|
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
|
||||||
timeout.start(120000);
|
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
if (!timeout.isActive()) {
|
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
qWarning() << "[NetworkService] postMonitoringDump timeout";
|
addFormField(multiPart, "text", text);
|
||||||
|
if (!archive.isEmpty()) {
|
||||||
|
QHttpPart filePart;
|
||||||
|
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
|
||||||
|
filePart.setHeader(
|
||||||
|
QNetworkRequest::ContentDispositionHeader,
|
||||||
|
QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(archiveFilename)
|
||||||
|
);
|
||||||
|
filePart.setBody(archive);
|
||||||
|
multiPart->append(filePart);
|
||||||
|
}
|
||||||
|
if (!image.isEmpty()) {
|
||||||
|
QHttpPart imagePart;
|
||||||
|
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
|
||||||
|
imagePart.setHeader(
|
||||||
|
QNetworkRequest::ContentDispositionHeader,
|
||||||
|
QStringLiteral("form-data; name=\"image\"; filename=\"final_screen.png\"")
|
||||||
|
);
|
||||||
|
imagePart.setBody(image);
|
||||||
|
multiPart->append(imagePart);
|
||||||
|
}
|
||||||
|
|
||||||
|
reply = m_manager->post(request, multiPart);
|
||||||
|
multiPart->setParent(reply);
|
||||||
|
|
||||||
|
if (awaitReply(reply, kDumpTimeoutMs)) { timedOut = false; break; }
|
||||||
|
qWarning() << "[NetworkService] postMonitoringDump timeout attempt"
|
||||||
|
<< attempt << "/" << kMaxRequestAttempts;
|
||||||
reply->abort();
|
reply->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timedOut) {
|
||||||
|
qWarning() << "[NetworkService] postMonitoringDump timeout after"
|
||||||
|
<< kMaxRequestAttempts << "attempts";
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
qWarning() << "[NetworkService] postMonitoringDump failed:" << reply->errorString()
|
qWarning() << "[NetworkService] postMonitoringDump failed:" << reply->errorString()
|
||||||
<< "\n response body:" << reply->readAll();
|
<< "\n response body:" << reply->readAll();
|
||||||
@ -1634,12 +1669,8 @@ void NetworkService::scheduleNextStatusSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool NetworkService::runPeriodicSyncTick() {
|
bool NetworkService::runPeriodicSyncTick() {
|
||||||
// Фаза 2 — обёртка с грубым detect-успех/неудача. В фазе 3 syncCurrentProfile
|
|
||||||
// будет переписан и сам вернёт bool; пока считаем тик неуспешным только при
|
|
||||||
// протухшем токене (более тонкая детекция придёт с рефакторингом).
|
|
||||||
if (!ensureValidToken()) return false;
|
if (!ensureValidToken()) return false;
|
||||||
syncCurrentProfile();
|
return syncCurrentProfile();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QMutex NetworkService::s_keyRegistryMutex;
|
QMutex NetworkService::s_keyRegistryMutex;
|
||||||
|
|||||||
@ -59,7 +59,9 @@ public slots:
|
|||||||
void updateTransactionData();
|
void updateTransactionData();
|
||||||
|
|
||||||
void loginAndSetup();
|
void loginAndSetup();
|
||||||
void syncCurrentProfile();
|
// true — GET current_profile прошёл; false — таймаут/сеть. Используется
|
||||||
|
// health-счётчиком в runPeriodicSyncTick (правило #9).
|
||||||
|
bool syncCurrentProfile();
|
||||||
|
|
||||||
// Включает периодическую синхронизацию статусов (60s). Должно быть вызвано
|
// Включает периодическую синхронизацию статусов (60s). Должно быть вызвано
|
||||||
// ОДИН раз для долгоживущего инстанса после firstPassDone от DeviceScreener.
|
// ОДИН раз для долгоживущего инстанса после firstPassDone от DeviceScreener.
|
||||||
@ -81,8 +83,12 @@ public slots:
|
|||||||
|
|
||||||
void postTaskResult();
|
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,
|
void postMonitoringLog(const QString &type, const QString &event,
|
||||||
const QString &message, const QByteArray &image);
|
const QString &message, const QByteArray &image,
|
||||||
|
bool toApiBase = false);
|
||||||
|
|
||||||
void postMonitoringDump(const QString &text, const QByteArray &archive,
|
void postMonitoringDump(const QString &text, const QByteArray &archive,
|
||||||
const QByteArray &image,
|
const QByteArray &image,
|
||||||
|
|||||||
@ -1,27 +1,7 @@
|
|||||||
{
|
{
|
||||||
"bank_profiles": [
|
"bank_profiles": [
|
||||||
{
|
{
|
||||||
"id": "047fd3d6-f2a2-40e0-9a39-1d11422e8832",
|
"id": "9b2f1a38-190c-4cb0-b026-eef721db36ed",
|
||||||
"bank_name": "dushanbe_city_bank",
|
|
||||||
"state": "enabled",
|
|
||||||
"phone_number": "+992185555519",
|
|
||||||
"first_name": "Мехрубон",
|
|
||||||
"middle_name": null,
|
|
||||||
"last_name": "Имомкулзода",
|
|
||||||
"currency": 972,
|
|
||||||
"device_id": "986cb259-dfec-4421-8735-4fb126cd1917",
|
|
||||||
"device_label": "Pixel 7",
|
|
||||||
"materials": [
|
|
||||||
{
|
|
||||||
"id": "83606831-7688-42cf-b2d7-2034e04f68f2",
|
|
||||||
"state": "enabled",
|
|
||||||
"card_number": "9762000207113258",
|
|
||||||
"currency": 972
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "21aa26c8-ea89-45ea-a283-ee6d55ead82b",
|
|
||||||
"bank_name": "ozon",
|
"bank_name": "ozon",
|
||||||
"state": "enabled",
|
"state": "enabled",
|
||||||
"phone_number": "+79538038318",
|
"phone_number": "+79538038318",
|
||||||
@ -29,33 +9,13 @@
|
|||||||
"middle_name": null,
|
"middle_name": null,
|
||||||
"last_name": "Терехина",
|
"last_name": "Терехина",
|
||||||
"currency": 643,
|
"currency": 643,
|
||||||
"device_id": "986cb259-dfec-4421-8735-4fb126cd1917",
|
"device_id": "cca4ec91-26e4-42e0-81ce-5226382b6cbc",
|
||||||
"device_label": "Pixel 7",
|
"device_label": "Pixel 7",
|
||||||
"materials": [
|
"materials": [
|
||||||
{
|
{
|
||||||
"id": "3a2ca76d-f735-4006-b521-66bccaf821a4",
|
"id": "e1234f11-b99d-4930-9f86-75148601f683",
|
||||||
"state": "enabled",
|
"state": "enabled",
|
||||||
"card_number": "2204321086109865",
|
"card_number": "2204321035207919",
|
||||||
"currency": 643
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "524c4cfe-9522-4434-90d1-9ee80aae92b2",
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"state": "enabled",
|
|
||||||
"phone_number": "+79855532896",
|
|
||||||
"first_name": "Владислав",
|
|
||||||
"middle_name": null,
|
|
||||||
"last_name": "Касаткин",
|
|
||||||
"currency": 643,
|
|
||||||
"device_id": "ec6397fb-89b1-41e1-9b6b-303d6f1e943f",
|
|
||||||
"device_label": "TECNO KM4",
|
|
||||||
"materials": [
|
|
||||||
{
|
|
||||||
"id": "19d1a302-7155-4d68-b596-5c7fb7cf6c2e",
|
|
||||||
"state": "enabled",
|
|
||||||
"card_number": "2204320439130982",
|
|
||||||
"currency": 643
|
"currency": 643
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "3a2ca76d-f735-4006-b521-66bccaf821a4",
|
|
||||||
"type": "FETCH_PROFILE",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "19d1a302-7155-4d68-b596-5c7fb7cf6c2e",
|
|
||||||
"type": "FETCH_PROFILE",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
@ -15,7 +15,6 @@
|
|||||||
#include "TutorialWindow.h"
|
#include "TutorialWindow.h"
|
||||||
#include "bank/BankListWindow.h"
|
#include "bank/BankListWindow.h"
|
||||||
#include "event/EventWindow.h"
|
#include "event/EventWindow.h"
|
||||||
#include "log/LogWindow.h"
|
|
||||||
#include "log/FileLogWindow.h"
|
#include "log/FileLogWindow.h"
|
||||||
#include "dao/GeneralLogDAO.h"
|
#include "dao/GeneralLogDAO.h"
|
||||||
#include "net/NetworkService.h"
|
#include "net/NetworkService.h"
|
||||||
|
|||||||
@ -1,154 +0,0 @@
|
|||||||
#include "LogWindow.h"
|
|
||||||
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QHeaderView>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QResizeEvent>
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QSplitter>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#include "dao/AppLogDAO.h"
|
|
||||||
|
|
||||||
static constexpr int COL_ID = 0;
|
|
||||||
static constexpr int COL_TIME = 1;
|
|
||||||
static constexpr int COL_SOURCE = 2;
|
|
||||||
static constexpr int COL_DEVICE = 3;
|
|
||||||
static constexpr int COL_MESSAGE = 4;
|
|
||||||
|
|
||||||
LogWindow::LogWindow(QWidget *parent) : QDialog(parent) {
|
|
||||||
setWindowTitle("Логи ошибок");
|
|
||||||
resize(1100, 650);
|
|
||||||
|
|
||||||
// ── Таблица ───────────────────────────────────────────────────────────────
|
|
||||||
m_table = new QTableWidget(this);
|
|
||||||
m_table->setColumnCount(5);
|
|
||||||
m_table->setHorizontalHeaderLabels({"#", "Время", "Источник", "Устройство", "Сообщение"});
|
|
||||||
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
||||||
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
||||||
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
||||||
m_table->verticalHeader()->hide();
|
|
||||||
m_table->horizontalHeader()->setStretchLastSection(true);
|
|
||||||
m_table->setColumnWidth(COL_ID, 45);
|
|
||||||
m_table->setColumnWidth(COL_TIME, 145);
|
|
||||||
m_table->setColumnWidth(COL_SOURCE, 170);
|
|
||||||
m_table->setColumnWidth(COL_DEVICE, 120);
|
|
||||||
|
|
||||||
// ── Панель скриншота ──────────────────────────────────────────────────────
|
|
||||||
m_screenshotLabel = new QLabel("Выберите строку для просмотра скриншота");
|
|
||||||
m_screenshotLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
m_screenshotLabel->setMinimumWidth(320);
|
|
||||||
m_screenshotLabel->setWordWrap(true);
|
|
||||||
m_screenshotLabel->setStyleSheet("background: #1a1a1a; color: #888; border-radius: 4px;");
|
|
||||||
|
|
||||||
auto *scrollArea = new QScrollArea(this);
|
|
||||||
scrollArea->setWidget(m_screenshotLabel);
|
|
||||||
scrollArea->setWidgetResizable(true);
|
|
||||||
scrollArea->setMinimumWidth(320);
|
|
||||||
|
|
||||||
// ── Сплиттер ─────────────────────────────────────────────────────────────
|
|
||||||
auto *splitter = new QSplitter(Qt::Horizontal, this);
|
|
||||||
splitter->addWidget(m_table);
|
|
||||||
splitter->addWidget(scrollArea);
|
|
||||||
splitter->setStretchFactor(0, 7);
|
|
||||||
splitter->setStretchFactor(1, 3);
|
|
||||||
|
|
||||||
// ── Пагинация ─────────────────────────────────────────────────────────────
|
|
||||||
m_prevBtn = new QPushButton("←", this);
|
|
||||||
m_nextBtn = new QPushButton("→", this);
|
|
||||||
m_pageLabel = new QLabel(this);
|
|
||||||
m_pageLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
m_pageLabel->setMinimumWidth(140);
|
|
||||||
|
|
||||||
auto *refreshBtn = new QPushButton("Обновить", this);
|
|
||||||
|
|
||||||
auto *paginationLayout = new QHBoxLayout;
|
|
||||||
paginationLayout->addWidget(m_prevBtn);
|
|
||||||
paginationLayout->addWidget(m_pageLabel);
|
|
||||||
paginationLayout->addWidget(m_nextBtn);
|
|
||||||
paginationLayout->addStretch();
|
|
||||||
paginationLayout->addWidget(refreshBtn);
|
|
||||||
|
|
||||||
auto *mainLayout = new QVBoxLayout(this);
|
|
||||||
mainLayout->addWidget(splitter, 1);
|
|
||||||
mainLayout->addLayout(paginationLayout);
|
|
||||||
|
|
||||||
// ── Сигналы ───────────────────────────────────────────────────────────────
|
|
||||||
connect(m_prevBtn, &QPushButton::clicked, this, [this]() {
|
|
||||||
if (m_page > 0) { --m_page; loadPage(); }
|
|
||||||
});
|
|
||||||
connect(m_nextBtn, &QPushButton::clicked, this, [this]() {
|
|
||||||
if (m_page < m_totalPages - 1) { ++m_page; loadPage(); }
|
|
||||||
});
|
|
||||||
connect(refreshBtn, &QPushButton::clicked, this, [this]() {
|
|
||||||
m_page = 0;
|
|
||||||
loadPage();
|
|
||||||
});
|
|
||||||
connect(m_table, &QTableWidget::currentCellChanged, this,
|
|
||||||
[this](int row, int, int, int) {
|
|
||||||
if (row < 0) return;
|
|
||||||
const int logId = m_table->item(row, COL_ID)->text().toInt();
|
|
||||||
showScreenshot(logId);
|
|
||||||
});
|
|
||||||
|
|
||||||
loadPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LogWindow::loadPage() {
|
|
||||||
const int total = AppLogDAO::getTotalCount();
|
|
||||||
m_totalPages = qMax(1, (total + m_pageSize - 1) / m_pageSize);
|
|
||||||
m_page = qBound(0, m_page, m_totalPages - 1);
|
|
||||||
|
|
||||||
const QList<AppLogEntry> entries = AppLogDAO::getLogs(m_page, m_pageSize);
|
|
||||||
|
|
||||||
m_table->setRowCount(static_cast<int>(entries.size()));
|
|
||||||
for (int i = 0; i < entries.size(); ++i) {
|
|
||||||
const auto &e = entries[i];
|
|
||||||
m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id)));
|
|
||||||
m_table->setItem(i, COL_TIME, new QTableWidgetItem(
|
|
||||||
e.timestamp.toLocalTime().toString("dd.MM.yy HH:mm:ss")));
|
|
||||||
m_table->setItem(i, COL_SOURCE, new QTableWidgetItem(e.source));
|
|
||||||
m_table->setItem(i, COL_DEVICE, new QTableWidgetItem(e.deviceId));
|
|
||||||
m_table->setItem(i, COL_MESSAGE, new QTableWidgetItem(e.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сбрасываем скриншот при смене страницы
|
|
||||||
m_screenshotLabel->setPixmap({});
|
|
||||||
m_screenshotLabel->setText("Выберите строку для просмотра скриншота");
|
|
||||||
|
|
||||||
updatePagination();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LogWindow::updatePagination() {
|
|
||||||
m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages));
|
|
||||||
m_prevBtn->setEnabled(m_page > 0);
|
|
||||||
m_nextBtn->setEnabled(m_page < m_totalPages - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LogWindow::showScreenshot(int logId) {
|
|
||||||
const QByteArray bytes = AppLogDAO::getScreenshot(logId);
|
|
||||||
if (bytes.isEmpty()) {
|
|
||||||
m_originalPixmap = {};
|
|
||||||
m_screenshotLabel->setPixmap({});
|
|
||||||
m_screenshotLabel->setText("Скриншот отсутствует");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!m_originalPixmap.loadFromData(bytes)) {
|
|
||||||
m_originalPixmap = {};
|
|
||||||
m_screenshotLabel->setText("Не удалось загрузить скриншот");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rescaleScreenshot();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LogWindow::rescaleScreenshot() {
|
|
||||||
if (m_originalPixmap.isNull()) return;
|
|
||||||
m_screenshotLabel->setPixmap(
|
|
||||||
m_originalPixmap.scaled(m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LogWindow::resizeEvent(QResizeEvent *event) {
|
|
||||||
QDialog::resizeEvent(event);
|
|
||||||
rescaleScreenshot();
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <QDialog>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QTableWidget>
|
|
||||||
|
|
||||||
class LogWindow final : public QDialog {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit LogWindow(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QTableWidget *m_table;
|
|
||||||
QLabel *m_screenshotLabel;
|
|
||||||
QLabel *m_pageLabel;
|
|
||||||
QPushButton *m_prevBtn;
|
|
||||||
QPushButton *m_nextBtn;
|
|
||||||
|
|
||||||
int m_page = 0;
|
|
||||||
int m_pageSize = 50;
|
|
||||||
int m_totalPages = 1;
|
|
||||||
|
|
||||||
QPixmap m_originalPixmap;
|
|
||||||
|
|
||||||
void loadPage();
|
|
||||||
void updatePagination();
|
|
||||||
void showScreenshot(int logId);
|
|
||||||
void rescaleScreenshot();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue
Block a user