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.
This commit is contained in:
parent
80be0ee7e9
commit
d7886dbdcf
@ -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 = {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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() {
|
||||
|
||||
Binary file not shown.
70
database/dao/AppLogDAO.cpp
Normal file
70
database/dao/AppLogDAO.cpp
Normal file
@ -0,0 +1,70 @@
|
||||
#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();
|
||||
}
|
||||
17
database/dao/AppLogDAO.h
Normal file
17
database/dao/AppLogDAO.h
Normal file
@ -0,0 +1,17 @@
|
||||
#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);
|
||||
};
|
||||
104
docs/api/network.md
Normal file
104
docs/api/network.md
Normal file
@ -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 <access_token>` из `config.ini [common/access_token]`
|
||||
- **Legacy** — поле `token` в multipart-теле (из `config.ini [common/token]`)
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -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`.
|
||||
Гарантирует что одна и та же задача не попадёт в очередь дважды
|
||||
пока не завершится предыдущее выполнение.
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
| [Profile & Material](api/profile.md) | Банковские профили и карты (материалы) |
|
||||
| [Device & Desktop](api/device.md) | Регистрация десктопов и Android-устройств, скриншоты |
|
||||
| [Task](api/task.md) | Получение задач, отправка результатов |
|
||||
| [NetworkService](api/network.md) | Полная карта всех HTTP-вызовов приложения |
|
||||
|
||||
---
|
||||
|
||||
|
||||
13
models/db/AppLogEntry.h
Normal file
13
models/db/AppLogEntry.h
Normal file
@ -0,0 +1,13 @@
|
||||
#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;
|
||||
};
|
||||
17
services/AppLogger.cpp
Normal file
17
services/AppLogger.cpp
Normal file
@ -0,0 +1,17 @@
|
||||
#include "AppLogger.h"
|
||||
|
||||
#include <QDateTime>
|
||||
|
||||
#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);
|
||||
}
|
||||
13
services/AppLogger.h
Normal file
13
services/AppLogger.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
|
||||
class AppLogger {
|
||||
public:
|
||||
static void log(
|
||||
const QString &source,
|
||||
const QString &deviceId,
|
||||
const QString &message,
|
||||
const QByteArray &screenshot = {}
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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]() {
|
||||
|
||||
@ -27,6 +27,7 @@ private:
|
||||
QWidget *m_accountsWindow = nullptr;
|
||||
QWidget *m_tutorialWindow = nullptr;
|
||||
QWidget *m_bankListWindow = nullptr;
|
||||
QWidget *m_logWindow = nullptr;
|
||||
|
||||
void createDevicePage();
|
||||
|
||||
|
||||
142
views/log/LogWindow.cpp
Normal file
142
views/log/LogWindow.cpp
Normal file
@ -0,0 +1,142 @@
|
||||
#include "LogWindow.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QPixmap>
|
||||
#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_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)
|
||||
);
|
||||
}
|
||||
27
views/log/LogWindow.h
Normal file
27
views/log/LogWindow.h
Normal file
@ -0,0 +1,27 @@
|
||||
#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;
|
||||
|
||||
void loadPage();
|
||||
void updatePagination();
|
||||
void showScreenshot(int logId);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user