From 73e23f437dde478a539574abb295fa733807b893 Mon Sep 17 00:00:00 2001 From: slava Date: Sun, 1 Mar 2026 12:02:37 +0700 Subject: [PATCH] Add Ozon `pay_to_new_number` and `pay_to_old_number` XML assets Included XML files for the "pay to new number" and "pay to old number" features in the Ozon platform. --- android/adb/AdbUtils.cpp | 15 ++ android/adb/AdbUtils.h | 3 + android/ozon/GetCardInfoScript.cpp | 3 + android/ozon/GetLastTransactionsScript.cpp | 3 + android/ozon/LoginAndCheckAccountsScript.cpp | 3 + config.ini | 6 +- database/DatabaseManager.cpp | 20 +++ database/app_data.db | Bin 3870720 -> 3870720 bytes database/dao/AppLogDAO.cpp | 70 +++++++++ database/dao/AppLogDAO.h | 17 +++ docs/api/network.md | 104 ++++++++++++++ docs/api/task.md | 19 +-- docs/events.md | 20 +-- docs/index.md | 1 + models/db/AppLogEntry.h | 13 ++ services/AppLogger.cpp | 17 +++ services/AppLogger.h | 13 ++ services/EventHandler.cpp | 2 +- services/net/NetworkService.cpp | 4 +- views/MainWindow.cpp | 12 ++ views/MainWindow.h | 1 + views/log/LogWindow.cpp | 142 +++++++++++++++++++ views/log/LogWindow.h | 27 ++++ 23 files changed, 492 insertions(+), 23 deletions(-) create mode 100644 database/dao/AppLogDAO.cpp create mode 100644 database/dao/AppLogDAO.h create mode 100644 docs/api/network.md create mode 100644 models/db/AppLogEntry.h create mode 100644 services/AppLogger.cpp create mode 100644 services/AppLogger.h create mode 100644 views/log/LogWindow.cpp create mode 100644 views/log/LogWindow.h diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp index 3632941..c4a4880 100644 --- a/android/adb/AdbUtils.cpp +++ b/android/adb/AdbUtils.cpp @@ -194,6 +194,21 @@ bool AdbUtils::takeScreenshot(const QString &deviceId, const QString &name) { return true; } +QByteArray AdbUtils::captureScreenshotBytes(const QString &deviceId) { + const QString cmd = QString("%1 -s %2 exec-out screencap -p").arg(adbPath(), deviceId); + QProcess process; +#ifdef Q_OS_WIN + process.start("cmd.exe", {"/C", cmd}); +#else + process.start("sh", {"-c", cmd}); +#endif + if (!process.waitForFinished(10000) || process.exitCode() != 0) { + qWarning() << "[AdbUtils] captureScreenshotBytes failed for" << deviceId; + return {}; + } + return process.readAllStandardOutput(); +} + bool AdbUtils::takeXmlDump(const QString &deviceId, const QString &name) { const QString adb = adbPath(); QStringList commands = { diff --git a/android/adb/AdbUtils.h b/android/adb/AdbUtils.h index 7e26b65..0dc639d 100644 --- a/android/adb/AdbUtils.h +++ b/android/adb/AdbUtils.h @@ -23,6 +23,9 @@ public: static bool takeScreenshot(const QString &deviceId, const QString &name); + // Захватывает скриншот напрямую в память (PNG-байты), без временного файла + static QByteArray captureScreenshotBytes(const QString &deviceId); + static bool takeXmlDump(const QString &deviceId, const QString &name); static bool makeTap(const QString &deviceId, int x, int y); diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp index 874b5ec..d41dcd2 100644 --- a/android/ozon/GetCardInfoScript.cpp +++ b/android/ozon/GetCardInfoScript.cpp @@ -11,6 +11,7 @@ #include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" #include "net/NetworkService.h" +#include "AppLogger.h" namespace Ozon { @@ -59,6 +60,8 @@ void GetCardInfoScript::doStart() { device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + m_deviceId; qWarning() << m_error; + AppLogger::log("ozon/GetCardInfo", m_deviceId, m_error, + AdbUtils::captureScreenshotBytes(m_deviceId)); AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; diff --git a/android/ozon/GetLastTransactionsScript.cpp b/android/ozon/GetLastTransactionsScript.cpp index f1358b5..e2737c4 100644 --- a/android/ozon/GetLastTransactionsScript.cpp +++ b/android/ozon/GetLastTransactionsScript.cpp @@ -10,6 +10,7 @@ #include "dao/DeviceDAO.h" #include "dao/TransactionDAO.h" #include "time/DateUtils.h" +#include "AppLogger.h" namespace Ozon { @@ -53,6 +54,8 @@ void GetLastTransactionsScript::doStart() { device.screenWidth, device.screenHeight)) { m_error = "Cannot reach home screen: " + m_deviceId; qWarning() << m_error; + AppLogger::log("ozon/GetLastTransactions", m_deviceId, m_error, + AdbUtils::captureScreenshotBytes(m_deviceId)); AdbUtils::tryToKillApplication(m_deviceId, app.package); emit finishedWithResult(m_error); return; diff --git a/android/ozon/LoginAndCheckAccountsScript.cpp b/android/ozon/LoginAndCheckAccountsScript.cpp index c59ecda..583bab3 100644 --- a/android/ozon/LoginAndCheckAccountsScript.cpp +++ b/android/ozon/LoginAndCheckAccountsScript.cpp @@ -6,6 +6,7 @@ #include "dao/AccountDAO.h" #include "dao/ApplicationDAO.h" #include "dao/DeviceDAO.h" +#include "AppLogger.h" #include "GetCardInfoScript.h" namespace Ozon { @@ -38,6 +39,8 @@ void LoginAndCheckAccountsScript::doStart() { if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode; qWarning() << m_error; + AppLogger::log("ozon/LoginAndCheck", m_deviceId, m_error, + AdbUtils::captureScreenshotBytes(m_deviceId)); if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) { qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode; } diff --git a/config.ini b/config.ini index 9437b48..162eedf 100644 --- a/config.ini +++ b/config.ini @@ -2,13 +2,13 @@ list=ozon [common] -access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyNDE4MDUwfQ.26k6Z22gMOzNFuS17Mx0d4dt8pnJQh9TIv0rrAk2Xro +access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyNDI3Njk5fQ.OYKZQ6znq88jCyKhHVFjfowkpYTMHlDNP8shh4nGxJE api_key=0eee77b62013253e02e1a69bc6a505bb37757fb7 currencies=USD, KRW, AZN, RUB desktop_id=b86eac8e-dc89-4a9e-a5e8-7f85db011391 profile_id=c4552156-d179-4a79-9e91-587038c95cf0 -refresh_token=ff49cde7-4f3a-4efa-ad46-84514fc03bf6 -token_expires_at=1772418050 +refresh_token=f9016b28-4dd5-429f-868e-8a8e0b246498 +token_expires_at=1772427699 version=0.0.1 [db] diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index 2b24146..b0f4870 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -300,6 +300,26 @@ void DatabaseManager::initSchema() { q.exec("PRAGMA user_version = 9"); qDebug() << "[DB] Migration 9 applied"; } + + // ── Версия 10: таблица app_logs ─────────────────────────────────────────── + if (version < 10) { + db.transaction(); + + q.exec(R"(CREATE TABLE IF NOT EXISTS app_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source TEXT NOT NULL DEFAULT '', + device_id TEXT NOT NULL DEFAULT '', + message TEXT NOT NULL, + screenshot BLOB, + timestamp TEXT NOT NULL + ))"); + + q.exec("CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC)"); + + db.commit(); + q.exec("PRAGMA user_version = 10"); + qDebug() << "[DB] Migration 10 applied"; + } } void DatabaseManager::closeConnection() { diff --git a/database/app_data.db b/database/app_data.db index 9b787cda592ef5b9fd4ee62f3c5b5c402979cf6e..2fcaa97a470f3bd76859874274fe42e248f8f0f6 100644 GIT binary patch delta 702 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 +#include +#include + +#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 AppLogDAO::getLogs(int page, int pageSize) { + QList 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(); +} diff --git a/database/dao/AppLogDAO.h b/database/dao/AppLogDAO.h new file mode 100644 index 0000000..dec1863 --- /dev/null +++ b/database/dao/AppLogDAO.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include "db/AppLogEntry.h" + +class AppLogDAO { +public: + [[nodiscard]] static bool insert(const AppLogEntry &entry); + + // Возвращает записи без BLOB-скриншота (лёгкая загрузка для таблицы) + [[nodiscard]] static QList getLogs(int page, int pageSize); + + [[nodiscard]] static int getTotalCount(); + + // Загружает скриншот отдельно по id (по клику в UI) + [[nodiscard]] static QByteArray getScreenshot(int id); +}; diff --git a/docs/api/network.md b/docs/api/network.md new file mode 100644 index 0000000..0a9258b --- /dev/null +++ b/docs/api/network.md @@ -0,0 +1,104 @@ +# NetworkService — полная карта API-вызовов + +[← Главная](../index.md) + +Все HTTP-вызовы, выполняемые приложением. Класс `NetworkService` (`services/net/NetworkService.h/cpp`). + +--- + +## Аутентификация + +| Метод | Путь | Слот | Описание | +|-------|------|------|----------| +| POST | `/api/v1/auth/login` | `loginAndSetup()` | Вход по `api_key`, получение токенов | +| POST | `/api/v1/auth/refresh_token` | `refreshToken()` (private) | Обновление access_token | + +Токены сохраняются в `config.ini`. Авто-рефреш за 30 минут до истечения — `scheduleNextRefresh()`. + +--- + +## Desktop + +| Метод | Путь | Слот | Описание | +|-------|------|------|----------| +| POST | `/api/v1/desktop/` | `loginAndSetup()` | Создание desktop при первом запуске (если `desktop_id` не сохранён) | + +Ответ: сохраняет `desktop_id` и `profile_id` в `config.ini`. + +--- + +## Device + +| Метод | Путь | Слот | Описание | +|-------|------|------|----------| +| POST | `/api/v1/device/` | `createDevice()` | Регистрация нового Android устройства | +| PATCH | `/api/v1/device/{api_id}` | `updateDevice()` | Обновление статуса/батареи устройства | +| POST | `/api/v1/device/add_screenshot/{api_id}` | `postDeviceScreenshot()` | Загрузка скриншота (`multipart/form-data`, `image/jpeg`) | + +`api_id` — UUID, получаемый при создании и сохраняемый в таблице `devices`. + +--- + +## Profile / Material + +| Метод | Путь | Слот | Описание | +|-------|------|------|----------| +| POST | `/api/v1/profile/bank_profile` | `postBankProfile()` | Создать банк-профиль | +| PATCH | `/api/v1/profile/bank_profile/{id}` | `patchBankProfile()` | Обновить банк-профиль | +| POST | `/api/v1/profile/material` | `postMaterial()` | Создать материал (карту) | + +`postBankProfile` сохраняет `bank_profile_id` в таблице `applications`. +`postMaterial` сохраняет `material_id` (UUID) в таблице `accounts`. + +--- + +## Tasks + +| Метод | Путь | Слот | Auth | Описание | +|-------|------|------|------|----------| +| GET | `/api/v1/task/get_tasks` | `getTasks()` | Bearer | Получить очередь задач (поллинг каждые 5 сек) | +| POST | `/api/v1/task/task_result` | `postTaskResult()` | Bearer | Отправить результат задачи | + +`getTasks()` — асинхронный (без QEventLoop), использует сигнал `tasksReceived(QJsonArray)`. +`postTaskResult()` — вызывается из `sendTaskResult()` в `EventHandler` в отдельном QThread. + +--- + +## Устаревшие эндпоинты (legacy) + +Используют старый `multipart/form-data` API с токеном в теле. Пути задаются в `config.ini` напрямую как полные URL. + +| Слот | Ключ в config.ini | Статус | +|------|-------------------|--------| +| `postEventStatus()` | `net/eventStatusUpdate` | ⚠️ устарел, используется в `updateEventThread` | +| `postAppStatus()` | `net/appStatusUpdate` | используется при смене статуса приложения | +| `insertTransactionData()` | `net/transactionInsert` | создание транзакции | +| `updateTransactionData()` | `net/transactionUpdate` | обновление транзакции | +| `getPayments()` | `net/paymentsGet` | ⚠️ старый поллинг задач (заменён на `getTasks`) | + +--- + +## Конфигурация путей (`config.ini`) + +```ini +[net] +apiBase = http://localhost:9999 +pathAuthLogin = /api/v1/auth/login +pathAuthRefresh = /api/v1/auth/refresh_token +pathDesktopCreate = /api/v1/desktop/ +pathDevice = /api/v1/device/ +pathDeviceScreenshot = /api/v1/device/add_screenshot/ +pathBankProfile = /api/v1/profile/bank_profile +pathMaterial = /api/v1/profile/material +pathGetTasks = /api/v1/task/get_tasks +pathTaskResult = /api/v1/task/task_result +``` + +Если ключ отсутствует — используется значение по умолчанию (захардкожено в конструкторе `NetworkService`). + +--- + +## Авторизация в запросах + +- **Bearer** — `Authorization: Bearer ` из `config.ini [common/access_token]` +- **Legacy** — поле `token` в multipart-теле (из `config.ini [common/token]`) diff --git a/docs/api/task.md b/docs/api/task.md index 5ff6d04..fb4a7a2 100644 --- a/docs/api/task.md +++ b/docs/api/task.md @@ -17,19 +17,19 @@ "data": [ { "bank_name": "ozon", - "material_id": 42, + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", "type": "FETCH_PROFILE", "body": {} }, { "bank_name": "birbank", - "material_id": 7, + "material_id": "a1b2c3d4-0000-0000-0000-000000000001", "type": "FETCH_TRANSACTIONS", "body": {} }, { "bank_name": "ozon", - "material_id": 5, + "material_id": "a1b2c3d4-0000-0000-0000-000000000002", "type": "CREATE_TRANSACTION", "body": { "amount": 1000, @@ -43,6 +43,9 @@ } ``` +> `material_id` — UUID строка. Числовой формат поддерживается для обратной совместимости. +``` + **Типы задач:** | `type` | Описание | `body` | @@ -78,7 +81,7 @@ ```json { "type": "FETCH_PROFILE", - "material_id": 42, + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", "bank_name": "ozon", "data": { "bank_account_id": "string", @@ -94,7 +97,7 @@ ```json { "type": "FETCH_TRANSACTIONS", - "material_id": 7, + "material_id": "a1b2c3d4-0000-0000-0000-000000000001", "bank_name": "birbank", "data": [ { @@ -116,7 +119,7 @@ ```json { "type": "CREATE_TRANSACTION", - "material_id": 5, + "material_id": "a1b2c3d4-0000-0000-0000-000000000002", "bank_name": "ozon", "data": { "bank_transaction_id": "string", @@ -160,11 +163,11 @@ ```json { "type": "FETCH_PROFILE", - "material_id": 42, + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", "bank_name": "ozon", "data": { "status": "error", - "description": "Account not found for material_id: 42" + "description": "Account not found for material_id: c340cd2e-cefd-48d6-a813-c4be097ae493" } } ``` diff --git a/docs/events.md b/docs/events.md index 0e7961f..89c0259 100644 --- a/docs/events.md +++ b/docs/events.md @@ -61,8 +61,8 @@ QTimer (5s) │ YES → пропустить (дедупликация) │ NO → продолжить │ - ├─ AccountDAO::getAccountsByAppCode(bank_name) - │ найти аккаунт где materialId == material_id + ├─ AccountDAO::findByMaterialId(material_id) + │ прямой SELECT WHERE material_id = ? │ НЕ НАЙДЕН → sendTaskResult(status=error) и пропустить │ └─ EventDAO::insertEvent(status=Wait) @@ -128,11 +128,11 @@ result != "" → EventDAO::updateEvent(Error, message) ```json { "type": "FETCH_PROFILE", - "material_id": 42, + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", "bank_name": "ozon", "data": { "status": "error", - "description": "Account not found for material_id: 42" + "description": "Account not found for material_id: c340cd2e-cefd-48d6-a813-c4be097ae493" } } ``` @@ -142,7 +142,7 @@ result != "" → EventDAO::updateEvent(Error, message) ```json { "type": "FETCH_PROFILE", - "material_id": 42, + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", "bank_name": "ozon", "data": { "bank_account_id": "string", @@ -158,7 +158,7 @@ result != "" → EventDAO::updateEvent(Error, message) ```json { "type": "FETCH_TRANSACTIONS", - "material_id": 7, + "material_id": "a1b2c3d4-0000-0000-0000-000000000001", "bank_name": "birbank", "data": [ { @@ -180,7 +180,7 @@ result != "" → EventDAO::updateEvent(Error, message) ```json { "type": "FETCH_PROFILE", - "material_id": 42, + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", "bank_name": "ozon", "data": { "status": "error", @@ -196,7 +196,7 @@ result != "" → EventDAO::updateEvent(Error, message) ```cpp struct EventInfo { int id; // локальный ID в SQLite - int externalId; // material_id из API + QString externalId; // material_id из API (UUID строка) int accountId; // FK → accounts QString deviceId; // ADB device id @@ -220,8 +220,8 @@ struct EventInfo { ## Дедупликация -`EventDAO::existsActiveByExternalId(int externalId)` — проверяет наличие записи -с `status IN ('wait', 'iPprogress')` для данного `external_id`. +`EventDAO::existsActiveByExternalId(const QString &externalId)` — проверяет наличие записи +с `status IN ('wait', 'inprogress')` для данного `external_id`. Гарантирует что одна и та же задача не попадёт в очередь дважды пока не завершится предыдущее выполнение. diff --git a/docs/index.md b/docs/index.md index 64f9d85..ec2194c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,6 +20,7 @@ | [Profile & Material](api/profile.md) | Банковские профили и карты (материалы) | | [Device & Desktop](api/device.md) | Регистрация десктопов и Android-устройств, скриншоты | | [Task](api/task.md) | Получение задач, отправка результатов | +| [NetworkService](api/network.md) | Полная карта всех HTTP-вызовов приложения | --- diff --git a/models/db/AppLogEntry.h b/models/db/AppLogEntry.h new file mode 100644 index 0000000..7656e57 --- /dev/null +++ b/models/db/AppLogEntry.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include + +struct AppLogEntry { + int id = -1; + QString source; // "ozon/LoginAndCheck", "ozon/GetLastTransactions", etc. + QString deviceId; // ADB device id, может быть пустым + QString message; // текст ошибки + QByteArray screenshot; // PNG-байты скриншота (может быть пустым) + QDateTime timestamp; +}; diff --git a/services/AppLogger.cpp b/services/AppLogger.cpp new file mode 100644 index 0000000..1da7e73 --- /dev/null +++ b/services/AppLogger.cpp @@ -0,0 +1,17 @@ +#include "AppLogger.h" + +#include + +#include "dao/AppLogDAO.h" +#include "db/AppLogEntry.h" + +void AppLogger::log(const QString &source, const QString &deviceId, + const QString &message, const QByteArray &screenshot) { + AppLogEntry entry; + entry.source = source; + entry.deviceId = deviceId; + entry.message = message; + entry.screenshot = screenshot; + entry.timestamp = QDateTime::currentDateTimeUtc(); + AppLogDAO::insert(entry); +} diff --git a/services/AppLogger.h b/services/AppLogger.h new file mode 100644 index 0000000..23dd387 --- /dev/null +++ b/services/AppLogger.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include + +class AppLogger { +public: + static void log( + const QString &source, + const QString &deviceId, + const QString &message, + const QByteArray &screenshot = {} + ); +}; diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index dd457d2..89e1a51 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -130,7 +130,7 @@ static void sendTaskResult(const QString &type, const QString &materialId, const thread->start(); } -static void sendEventStatus(const QString &deviceId, const int externalId, const int eventId, +static void sendEventStatus(const QString &deviceId, const QString &externalId, const int eventId, const QString &status, const QString &error) { QJsonObject obj; obj["id"] = externalId; diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index 3980f54..efa30c1 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -152,7 +152,9 @@ void NetworkService::getPayments() { EventInfo eventInfo; eventInfo.status = EventStatus::Wait; eventInfo.type = eventTypeFromString(obj["type"].toString()); - eventInfo.externalId = obj["id"].toInt(); + eventInfo.externalId = obj["id"].isString() + ? obj["id"].toString() + : QString::number(obj["id"].toInt()); eventInfo.timestamp = QDateTime::fromMSecsSinceEpoch(obj["timestamp"].toDouble(), QTimeZone::utc()); eventInfo.amount = obj["amount"].toDouble(); eventInfo.bankName = obj["bankName"].toString(); diff --git a/views/MainWindow.cpp b/views/MainWindow.cpp index d642bd5..4906b23 100644 --- a/views/MainWindow.cpp +++ b/views/MainWindow.cpp @@ -8,6 +8,7 @@ #include "TutorialWindow.h" #include "bank/BankListWindow.h" +#include "log/LogWindow.h" #include "db/DeviceInfo.h" #include "device/DeviceWidget.h" #include "dao/DeviceDAO.h" @@ -38,10 +39,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // auto *accountPageAction = new QAction("Аккаунты", this); auto *bankListAction = new QAction("Банки", this); auto *tutorialPageAction = new QAction("Инструкция", this); + auto *logAction = new QAction("Логи", this); menu->addAction(devicesPageAction); // menu->addAction(accountPageAction); menu->addAction(bankListAction); menu->addAction(tutorialPageAction); + menu->addAction(logAction); connect(devicesPageAction, &QAction::triggered, this, [=]() { if (!m_devicesWindow) { @@ -73,6 +76,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_tutorialWindow->activateWindow(); }); + connect(logAction, &QAction::triggered, this, [=]() { + if (!m_logWindow) { + m_logWindow = new LogWindow(this); + } + m_logWindow->show(); + m_logWindow->raise(); + m_logWindow->activateWindow(); + }); + // m_tutorialWindow = new TutorialWindow(this); // m_tutorialWindow->show(); // QTimer::singleShot(0, m_tutorialWindow, [w = m_tutorialWindow]() { diff --git a/views/MainWindow.h b/views/MainWindow.h index c5bbf99..1b20565 100644 --- a/views/MainWindow.h +++ b/views/MainWindow.h @@ -27,6 +27,7 @@ private: QWidget *m_accountsWindow = nullptr; QWidget *m_tutorialWindow = nullptr; QWidget *m_bankListWindow = nullptr; + QWidget *m_logWindow = nullptr; void createDevicePage(); diff --git a/views/log/LogWindow.cpp b/views/log/LogWindow.cpp new file mode 100644 index 0000000..62efc1d --- /dev/null +++ b/views/log/LogWindow.cpp @@ -0,0 +1,142 @@ +#include "LogWindow.h" + +#include +#include +#include +#include +#include +#include + +#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 entries = AppLogDAO::getLogs(m_page, m_pageSize); + + m_table->setRowCount(static_cast(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_screenshotLabel->setPixmap({}); + m_screenshotLabel->setText("Скриншот отсутствует"); + return; + } + QPixmap px; + if (!px.loadFromData(bytes)) { + m_screenshotLabel->setText("Не удалось загрузить скриншот"); + return; + } + m_screenshotLabel->setPixmap( + px.scaled(m_screenshotLabel->width(), 9999, Qt::KeepAspectRatio, Qt::SmoothTransformation) + ); +} diff --git a/views/log/LogWindow.h b/views/log/LogWindow.h new file mode 100644 index 0000000..23af751 --- /dev/null +++ b/views/log/LogWindow.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include +#include + +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; + + void loadPage(); + void updatePagination(); + void showScreenshot(int logId); +};