Add EventWindow, database migrations, and enhanced DAO methods
Implemented `EventWindow` for viewing events with pagination and table-based UI. Added database migration to remove `UNIQUE` constraint on `external_id` in `events`. Enhanced DAO methods for event retrieval, pagination, and transaction updates. Integrated OCR recognition in transaction scripts and streamlined event handling in services.
This commit is contained in:
parent
73e23f437d
commit
832fb03aac
@ -27,6 +27,7 @@ if (WIN32)
|
|||||||
elseif (APPLE)
|
elseif (APPLE)
|
||||||
# Укажи путь к Qt6
|
# Укажи путь к Qt6
|
||||||
set(CMAKE_PREFIX_PATH "/opt/homebrew/opt/qt")
|
set(CMAKE_PREFIX_PATH "/opt/homebrew/opt/qt")
|
||||||
|
include_directories("/opt/homebrew/include")
|
||||||
link_directories("/opt/homebrew/lib")
|
link_directories("/opt/homebrew/lib")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
@ -135,6 +136,8 @@ elseif (APPLE)
|
|||||||
Qt6::Sql
|
Qt6::Sql
|
||||||
Qt6::Xml
|
Qt6::Xml
|
||||||
Qt6::Network
|
Qt6::Network
|
||||||
|
tesseract
|
||||||
|
leptonica
|
||||||
)
|
)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,8 @@ void GetCardInfoScript::doStart() {
|
|||||||
m_error = "Cannot reach home screen: " + m_deviceId;
|
m_error = "Cannot reach home screen: " + m_deviceId;
|
||||||
qWarning() << m_error;
|
qWarning() << m_error;
|
||||||
AppLogger::log("ozon/GetCardInfo", m_deviceId, m_error,
|
AppLogger::log("ozon/GetCardInfo", m_deviceId, m_error,
|
||||||
AdbUtils::captureScreenshotBytes(m_deviceId));
|
AdbUtils::captureScreenshotBytes(m_deviceId),
|
||||||
|
"FETCH_PROFILE");
|
||||||
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
emit finishedWithResult(m_error);
|
emit finishedWithResult(m_error);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
#include "GetLastTransactionsScript.h"
|
#include "GetLastTransactionsScript.h"
|
||||||
|
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QTimeZone>
|
#include <QTimeZone>
|
||||||
|
|
||||||
|
#include <tesseract/baseapi.h>
|
||||||
|
#include <leptonica/allheaders.h>
|
||||||
|
|
||||||
#include "adb/AdbUtils.h"
|
#include "adb/AdbUtils.h"
|
||||||
#include "dao/AccountDAO.h"
|
#include "dao/AccountDAO.h"
|
||||||
#include "dao/ApplicationDAO.h"
|
#include "dao/ApplicationDAO.h"
|
||||||
@ -12,6 +16,46 @@
|
|||||||
#include "time/DateUtils.h"
|
#include "time/DateUtils.h"
|
||||||
#include "AppLogger.h"
|
#include "AppLogger.h"
|
||||||
|
|
||||||
|
static QString recognizeTextFromImage(const QByteArray &pngData) {
|
||||||
|
auto *api = new tesseract::TessBaseAPI();
|
||||||
|
if (api->Init(nullptr, "rus+eng")) {
|
||||||
|
qWarning() << "[OCR] Failed to init Tesseract";
|
||||||
|
delete api;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
|
||||||
|
static_cast<size_t>(pngData.size()));
|
||||||
|
if (!pix) {
|
||||||
|
qWarning() << "[OCR] Failed to read image";
|
||||||
|
api->End();
|
||||||
|
delete api;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
api->SetImage(pix);
|
||||||
|
char *text = api->GetUTF8Text();
|
||||||
|
QString result = QString::fromUtf8(text).trimmed();
|
||||||
|
delete[] text;
|
||||||
|
pixDestroy(&pix);
|
||||||
|
api->End();
|
||||||
|
delete api;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString extractTransactionId(const QString &ocrText) {
|
||||||
|
// "ID операции A60130542076330K0000040011670301"
|
||||||
|
QRegularExpression reId(R"(ID\s+операции\s+([A-Za-z0-9]+))");
|
||||||
|
if (auto m = reId.match(ocrText); m.hasMatch()) return m.captured(1);
|
||||||
|
|
||||||
|
// "№ Ф-2026-25235663"
|
||||||
|
QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))");
|
||||||
|
if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
namespace Ozon {
|
namespace Ozon {
|
||||||
|
|
||||||
GetLastTransactionsScript::GetLastTransactionsScript(
|
GetLastTransactionsScript::GetLastTransactionsScript(
|
||||||
@ -55,7 +99,8 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
m_error = "Cannot reach home screen: " + m_deviceId;
|
m_error = "Cannot reach home screen: " + m_deviceId;
|
||||||
qWarning() << m_error;
|
qWarning() << m_error;
|
||||||
AppLogger::log("ozon/GetLastTransactions", m_deviceId, m_error,
|
AppLogger::log("ozon/GetLastTransactions", m_deviceId, m_error,
|
||||||
AdbUtils::captureScreenshotBytes(m_deviceId));
|
AdbUtils::captureScreenshotBytes(m_deviceId),
|
||||||
|
"FETCH_TRANSACTIONS");
|
||||||
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||||
emit finishedWithResult(m_error);
|
emit finishedWithResult(m_error);
|
||||||
return;
|
return;
|
||||||
@ -259,12 +304,28 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
screencapProc.waitForFinished(5000);
|
screencapProc.waitForFinished(5000);
|
||||||
const QByteArray screenshot = screencapProc.readAllStandardOutput();
|
const QByteArray screenshot = screencapProc.readAllStandardOutput();
|
||||||
if (!screenshot.isEmpty()) {
|
if (!screenshot.isEmpty()) {
|
||||||
// Сохраняем в БД
|
|
||||||
TransactionInfo saved = TransactionDAO::findByBankTimeAndAmount(
|
TransactionInfo saved = TransactionDAO::findByBankTimeAndAmount(
|
||||||
accountId, tx.bankName, tx.bankTime, tx.amount);
|
accountId, tx.bankName, tx.bankTime, tx.amount);
|
||||||
if (saved.id != -1) {
|
if (saved.id != -1) {
|
||||||
TransactionDAO::updateReceiptImage(saved.id, screenshot);
|
TransactionDAO::updateReceiptImage(saved.id, screenshot);
|
||||||
qDebug() << "[GetLastTransactions] Receipt saved to DB, txId:" << saved.id;
|
qDebug() << "[GetLastTransactions] Receipt saved to DB, txId:" << saved.id;
|
||||||
|
|
||||||
|
// OCR: распознаём текст и достаём ID операции
|
||||||
|
const QString ocrText = recognizeTextFromImage(screenshot);
|
||||||
|
if (!ocrText.isEmpty()) {
|
||||||
|
qDebug() << "[GetLastTransactions] OCR result, txId:" << saved.id
|
||||||
|
<< "\n--- OCR TEXT START ---\n" << ocrText
|
||||||
|
<< "\n--- OCR TEXT END ---";
|
||||||
|
const QString txId = extractTransactionId(ocrText);
|
||||||
|
if (!txId.isEmpty()) {
|
||||||
|
TransactionDAO::updateBankTrExternalId(saved.id, txId);
|
||||||
|
qDebug() << "[GetLastTransactions] Extracted transaction ID:" << txId;
|
||||||
|
} else {
|
||||||
|
qWarning() << "[GetLastTransactions] Could not extract transaction ID from OCR text";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qWarning() << "[GetLastTransactions] OCR returned empty text";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "[GetLastTransactions] Screenshot is empty";
|
qWarning() << "[GetLastTransactions] Screenshot is empty";
|
||||||
|
|||||||
@ -40,7 +40,8 @@ void LoginAndCheckAccountsScript::doStart() {
|
|||||||
m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode;
|
m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode;
|
||||||
qWarning() << m_error;
|
qWarning() << m_error;
|
||||||
AppLogger::log("ozon/LoginAndCheck", m_deviceId, m_error,
|
AppLogger::log("ozon/LoginAndCheck", m_deviceId, m_error,
|
||||||
AdbUtils::captureScreenshotBytes(m_deviceId));
|
AdbUtils::captureScreenshotBytes(m_deviceId),
|
||||||
|
"FETCH_PROFILE");
|
||||||
if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) {
|
if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) {
|
||||||
qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode;
|
qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -439,10 +439,20 @@ QList<Node> ScreenXmlParser::findPinCodeNodes(const QString &pinCode) {
|
|||||||
// 6. PIN-сетка — строки ровно с 3 узлами (3 столбца)
|
// 6. PIN-сетка — строки ровно с 3 узлами (3 столбца)
|
||||||
// Строки 1-3: цифры 1-9
|
// Строки 1-3: цифры 1-9
|
||||||
// Строка 4: (биометрия) [0] (backspace) → берём средний элемент
|
// Строка 4: (биометрия) [0] (backspace) → берём средний элемент
|
||||||
|
qDebug() << "[findPinCodeNodes] clickable:" << clickable.size()
|
||||||
|
<< "minH:" << minH << "candidates:" << candidates.size() << "rows:" << rows.size();
|
||||||
|
for (int i = 0; i < rows.size(); ++i) {
|
||||||
|
QStringList info;
|
||||||
|
for (const Node &n : rows[i])
|
||||||
|
info << QString("(%1,%2 %3x%4 '%5')").arg(n.x()).arg(n.y()).arg(n.width()).arg(n.height()).arg(n.text.left(15));
|
||||||
|
qDebug() << "[findPinCodeNodes] row" << i << "size:" << rows[i].size() << info.join(" | ");
|
||||||
|
}
|
||||||
|
|
||||||
QList<QList<Node>> gridRows;
|
QList<QList<Node>> gridRows;
|
||||||
for (const QList<Node> &row: rows) {
|
for (const QList<Node> &row: rows) {
|
||||||
if (row.size() == 3) gridRows.append(row);
|
if (row.size() == 3) gridRows.append(row);
|
||||||
}
|
}
|
||||||
|
qDebug() << "[findPinCodeNodes] gridRows:" << gridRows.size() << "pinCode len:" << pinCode.size();
|
||||||
|
|
||||||
QMap<QString, Node> digitMap;
|
QMap<QString, Node> digitMap;
|
||||||
const QStringList digits19 = {"1", "2", "3", "4", "5", "6", "7", "8", "9"};
|
const QStringList digits19 = {"1", "2", "3", "4", "5", "6", "7", "8", "9"};
|
||||||
|
|||||||
@ -2,13 +2,13 @@
|
|||||||
list=ozon
|
list=ozon
|
||||||
|
|
||||||
[common]
|
[common]
|
||||||
access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyNDI3Njk5fQ.OYKZQ6znq88jCyKhHVFjfowkpYTMHlDNP8shh4nGxJE
|
access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyNzE0MTQyfQ.-074b8eEWeLgzoxxpq5o54G1npivuC_VWvu4NLEz_gY
|
||||||
api_key=0eee77b62013253e02e1a69bc6a505bb37757fb7
|
api_key=0eee77b62013253e02e1a69bc6a505bb37757fb7
|
||||||
currencies=USD, KRW, AZN, RUB
|
currencies=USD, KRW, AZN, RUB
|
||||||
desktop_id=b86eac8e-dc89-4a9e-a5e8-7f85db011391
|
desktop_id=b86eac8e-dc89-4a9e-a5e8-7f85db011391
|
||||||
profile_id=c4552156-d179-4a79-9e91-587038c95cf0
|
profile_id=c4552156-d179-4a79-9e91-587038c95cf0
|
||||||
refresh_token=f9016b28-4dd5-429f-868e-8a8e0b246498
|
refresh_token=def3036b-b62d-4550-8971-94d7ef57eba2
|
||||||
token_expires_at=1772427699
|
token_expires_at=1772714142
|
||||||
version=0.0.1
|
version=0.0.1
|
||||||
|
|
||||||
[db]
|
[db]
|
||||||
|
|||||||
@ -12,9 +12,17 @@ DatabaseManager::DatabaseManager() = default;
|
|||||||
DatabaseManager::~DatabaseManager() {
|
DatabaseManager::~DatabaseManager() {
|
||||||
QMutexLocker locker(&mutex);
|
QMutexLocker locker(&mutex);
|
||||||
|
|
||||||
|
// QSqlDatabase требует живого QCoreApplication — при завершении
|
||||||
|
// статический синглтон может разрушаться после QApplication
|
||||||
|
if (!QCoreApplication::instance()) {
|
||||||
|
connectionPool.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (auto it = connectionPool.begin(); it != connectionPool.end(); ++it) {
|
for (auto it = connectionPool.begin(); it != connectionPool.end(); ++it) {
|
||||||
QSqlDatabase::removeDatabase(it.key());
|
QSqlDatabase::removeDatabase(it.key());
|
||||||
}
|
}
|
||||||
|
connectionPool.clear();
|
||||||
qDebug() << "Все соединения закрыты";
|
qDebug() << "Все соединения закрыты";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,6 +328,39 @@ void DatabaseManager::initSchema() {
|
|||||||
q.exec("PRAGMA user_version = 10");
|
q.exec("PRAGMA user_version = 10");
|
||||||
qDebug() << "[DB] Migration 10 applied";
|
qDebug() << "[DB] Migration 10 applied";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Версия 11: убираем UNIQUE(external_id) из events ─────────────────────
|
||||||
|
if (version < 11) {
|
||||||
|
db.transaction();
|
||||||
|
|
||||||
|
q.exec(R"(CREATE TABLE IF NOT EXISTS events_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
account_id INTEGER,
|
||||||
|
external_id TEXT,
|
||||||
|
device_id TEXT,
|
||||||
|
amount REAL,
|
||||||
|
phone TEXT,
|
||||||
|
bank_name TEXT,
|
||||||
|
comment TEXT,
|
||||||
|
status TEXT,
|
||||||
|
type TEXT,
|
||||||
|
update_time DATETIME,
|
||||||
|
complete_time DATETIME,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE
|
||||||
|
))");
|
||||||
|
|
||||||
|
q.exec(R"(INSERT INTO events_new
|
||||||
|
SELECT * FROM events)");
|
||||||
|
|
||||||
|
q.exec("DROP TABLE events");
|
||||||
|
q.exec("ALTER TABLE events_new RENAME TO events");
|
||||||
|
|
||||||
|
db.commit();
|
||||||
|
q.exec("PRAGMA user_version = 11");
|
||||||
|
qDebug() << "[DB] Migration 11 applied";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatabaseManager::closeConnection() {
|
void DatabaseManager::closeConnection() {
|
||||||
|
|||||||
Binary file not shown.
@ -217,6 +217,28 @@ AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &a
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList AccountDAO::getActiveMaterialIds() {
|
||||||
|
QStringList ids;
|
||||||
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
query.prepare(R"(
|
||||||
|
SELECT a.material_id
|
||||||
|
FROM accounts a
|
||||||
|
JOIN applications app ON a.device_id = app.device_id AND a.app_code = app.code
|
||||||
|
WHERE app.status = 'active'
|
||||||
|
AND a.material_id IS NOT NULL AND a.material_id != ''
|
||||||
|
)");
|
||||||
|
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "Ошибка при получении active material_ids:" << query.lastError().text();
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (query.next()) {
|
||||||
|
ids << query.value(0).toString();
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
AccountInfo AccountDAO::findByMaterialId(const QString &materialId) {
|
AccountInfo AccountDAO::findByMaterialId(const QString &materialId) {
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
|
||||||
|
|||||||
@ -31,4 +31,6 @@ public:
|
|||||||
);
|
);
|
||||||
|
|
||||||
[[nodiscard]] static AccountInfo findByMaterialId(const QString &materialId);
|
[[nodiscard]] static AccountInfo findByMaterialId(const QString &materialId);
|
||||||
|
|
||||||
|
[[nodiscard]] static QStringList getActiveMaterialIds();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,6 +17,7 @@ EventInfo parseEvent(const QSqlQuery &query) {
|
|||||||
event.amount = query.value("amount").toDouble();
|
event.amount = query.value("amount").toDouble();
|
||||||
event.phone = query.value("phone").toString();
|
event.phone = query.value("phone").toString();
|
||||||
event.bankName = query.value("bank_name").toString();
|
event.bankName = query.value("bank_name").toString();
|
||||||
|
event.comment = query.value("comment").toString();
|
||||||
event.timestamp = DateUtils::fromUtcString(query.value("timestamp").toString());
|
event.timestamp = DateUtils::fromUtcString(query.value("timestamp").toString());
|
||||||
event.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());
|
event.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());
|
||||||
event.completeTime = DateUtils::fromUtcString(query.value("complete_time").toString());
|
event.completeTime = DateUtils::fromUtcString(query.value("complete_time").toString());
|
||||||
@ -101,6 +102,18 @@ bool EventDAO::updateEvent(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EventDAO::updateEventAmount(int id, double amount) {
|
||||||
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
query.prepare("UPDATE events SET amount = :amount WHERE id = :id");
|
||||||
|
query.bindValue(":amount", amount);
|
||||||
|
query.bindValue(":id", id);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qCritical() << "Ошибка при обновлении amount event:" << query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
QList<EventInfo> EventDAO::getAllEvents(const EventStatus &status) {
|
QList<EventInfo> EventDAO::getAllEvents(const EventStatus &status) {
|
||||||
QList<EventInfo> events;
|
QList<EventInfo> events;
|
||||||
|
|
||||||
@ -127,7 +140,8 @@ QList<EventInfo> EventDAO::getAllEvents(const EventStatus &status) {
|
|||||||
EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) {
|
EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) {
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
SELECT id, account_id, device_id, external_id, amount, phone, bank_name, status, update_time, complete_time, timestamp, type
|
SELECT id, account_id, device_id, external_id, amount, phone, bank_name,
|
||||||
|
status, type, comment, update_time, complete_time, timestamp
|
||||||
FROM events
|
FROM events
|
||||||
WHERE status = :status AND device_id = :device_id
|
WHERE status = :status AND device_id = :device_id
|
||||||
ORDER BY timestamp ASC
|
ORDER BY timestamp ASC
|
||||||
@ -147,6 +161,37 @@ EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<EventInfo> EventDAO::getEvents(int page, int pageSize) {
|
||||||
|
QList<EventInfo> events;
|
||||||
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
query.prepare(R"(
|
||||||
|
SELECT id, account_id, device_id, external_id, amount, phone, bank_name,
|
||||||
|
status, type, comment, update_time, complete_time, timestamp
|
||||||
|
FROM events
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT :limit OFFSET :offset
|
||||||
|
)");
|
||||||
|
query.bindValue(":limit", pageSize);
|
||||||
|
query.bindValue(":offset", page * pageSize);
|
||||||
|
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "Ошибка при получении getEvents:" << query.lastError().text();
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (query.next()) {
|
||||||
|
events.append(parseEvent(query));
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EventDAO::getTotalCount() {
|
||||||
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
query.exec("SELECT COUNT(*) FROM events");
|
||||||
|
if (query.next()) return query.value(0).toInt();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
bool EventDAO::existsActiveByExternalId(const QString &externalId) {
|
bool EventDAO::existsActiveByExternalId(const QString &externalId) {
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
|
|||||||
@ -17,4 +17,10 @@ public:
|
|||||||
[[nodiscard]] static EventInfo getFirstWaitEventByDeviceId(const QString &key);
|
[[nodiscard]] static EventInfo getFirstWaitEventByDeviceId(const QString &key);
|
||||||
|
|
||||||
[[nodiscard]] static bool existsActiveByExternalId(const QString &externalId);
|
[[nodiscard]] static bool existsActiveByExternalId(const QString &externalId);
|
||||||
|
|
||||||
|
[[nodiscard]] static bool updateEventAmount(int id, double amount);
|
||||||
|
|
||||||
|
[[nodiscard]] static QList<EventInfo> getEvents(int page, int pageSize);
|
||||||
|
|
||||||
|
[[nodiscard]] static int getTotalCount();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -286,6 +286,29 @@ QList<TransactionInfo> TransactionDAO::getTransactionsWithinHours(const int acco
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<TransactionInfo> TransactionDAO::getLastTransactions(const int accountId, const int limit) {
|
||||||
|
QList<TransactionInfo> list;
|
||||||
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
query.prepare(R"(
|
||||||
|
SELECT * FROM transactions
|
||||||
|
WHERE account_id = :accountId
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT :limit
|
||||||
|
)");
|
||||||
|
query.bindValue(":accountId", accountId);
|
||||||
|
query.bindValue(":limit", limit);
|
||||||
|
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "Ошибка получения последних транзакций: " << query.lastError().text();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
while (query.next()) {
|
||||||
|
list.append(parseTransaction(query));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
bool TransactionDAO::updateTransaction(
|
bool TransactionDAO::updateTransaction(
|
||||||
const int id,
|
const int id,
|
||||||
const TransactionStatus status,
|
const TransactionStatus status,
|
||||||
@ -348,6 +371,18 @@ bool TransactionDAO::updateExternalTransactionId(const int id, const int externa
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TransactionDAO::updateBankTrExternalId(const int id, const QString &bankTrExternalId) {
|
||||||
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
|
query.prepare("UPDATE transactions SET bank_tr_external_id = :val WHERE id = :id");
|
||||||
|
query.bindValue(":id", id);
|
||||||
|
query.bindValue(":val", bankTrExternalId);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "Ошибка при обновлении bank_tr_external_id:" << query.lastError().text();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool TransactionDAO::updateReceiptImage(const int id, const QByteArray &image) {
|
bool TransactionDAO::updateReceiptImage(const int id, const QByteArray &image) {
|
||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare("UPDATE transactions SET receipt_image = :image WHERE id = :id");
|
query.prepare("UPDATE transactions SET receipt_image = :image WHERE id = :id");
|
||||||
|
|||||||
@ -19,6 +19,8 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] static QList<TransactionInfo> getTransactionsWithinHours(int accountId, int hours);
|
[[nodiscard]] static QList<TransactionInfo> getTransactionsWithinHours(int accountId, int hours);
|
||||||
|
|
||||||
|
[[nodiscard]] static QList<TransactionInfo> getLastTransactions(int accountId, int limit);
|
||||||
|
|
||||||
[[nodiscard]] static bool updateTransaction(
|
[[nodiscard]] static bool updateTransaction(
|
||||||
int id, TransactionStatus status,
|
int id, TransactionStatus status,
|
||||||
const QString &oldDesc,
|
const QString &oldDesc,
|
||||||
@ -27,6 +29,8 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] static bool updateExternalTransactionId(int id, int externalId);
|
[[nodiscard]] static bool updateExternalTransactionId(int id, int externalId);
|
||||||
|
|
||||||
|
[[nodiscard]] static bool updateBankTrExternalId(int id, const QString &bankTrExternalId);
|
||||||
|
|
||||||
[[nodiscard]] static bool updateReceiptImage(int id, const QByteArray &image);
|
[[nodiscard]] static bool updateReceiptImage(int id, const QByteArray &image);
|
||||||
|
|
||||||
// Найти транзакцию по bank_name + bank_time + amount
|
// Найти транзакцию по bank_name + bank_time + amount
|
||||||
|
|||||||
8
main.cpp
8
main.cpp
@ -16,6 +16,7 @@
|
|||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
|
#include "AppLogger.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "net/NetworkService.h"
|
#include "net/NetworkService.h"
|
||||||
#include "ozon/PayByPhoneScript.h"
|
#include "ozon/PayByPhoneScript.h"
|
||||||
@ -100,6 +101,7 @@ void startPayByPhoneTest() {
|
|||||||
void startNetworkService(const QCoreApplication &app) {
|
void startNetworkService(const QCoreApplication &app) {
|
||||||
auto *paymentThread = new QThread;
|
auto *paymentThread = new QThread;
|
||||||
auto *networkService = new NetworkService;
|
auto *networkService = new NetworkService;
|
||||||
|
AppLogger::setNetworkService(networkService);
|
||||||
networkService->moveToThread(paymentThread);
|
networkService->moveToThread(paymentThread);
|
||||||
QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::start);
|
QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::start);
|
||||||
QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit);
|
QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit);
|
||||||
@ -209,7 +211,7 @@ int main(int argc, char *argv[]) {
|
|||||||
startNetworkService(app);
|
startNetworkService(app);
|
||||||
startDeviceScreener(app);
|
startDeviceScreener(app);
|
||||||
startEventHandler(app);
|
startEventHandler(app);
|
||||||
startPayByPhoneTest();
|
// startPayByPhoneTest();
|
||||||
}, Qt::QueuedConnection);
|
}, Qt::QueuedConnection);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -237,7 +239,7 @@ int main(int argc, char *argv[]) {
|
|||||||
startNetworkService(app);
|
startNetworkService(app);
|
||||||
startDeviceScreener(app);
|
startDeviceScreener(app);
|
||||||
startEventHandler(app);
|
startEventHandler(app);
|
||||||
startPayByPhoneTest();
|
// startPayByPhoneTest();
|
||||||
} else if (r == 0) {
|
} else if (r == 0) {
|
||||||
QSettings s("config.ini", QSettings::IniFormat);
|
QSettings s("config.ini", QSettings::IniFormat);
|
||||||
s.remove("common/api_key");
|
s.remove("common/api_key");
|
||||||
@ -258,7 +260,7 @@ int main(int argc, char *argv[]) {
|
|||||||
startNetworkService(app);
|
startNetworkService(app);
|
||||||
startDeviceScreener(app);
|
startDeviceScreener(app);
|
||||||
startEventHandler(app);
|
startEventHandler(app);
|
||||||
startPayByPhoneTest();
|
// startPayByPhoneTest();
|
||||||
} else if (result == 0) {
|
} else if (result == 0) {
|
||||||
QSettings s("config.ini", QSettings::IniFormat);
|
QSettings s("config.ini", QSettings::IniFormat);
|
||||||
s.remove("common/api_key");
|
s.remove("common/api_key");
|
||||||
|
|||||||
@ -1,12 +1,21 @@
|
|||||||
#include "AppLogger.h"
|
#include "AppLogger.h"
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <QMetaObject>
|
||||||
|
|
||||||
#include "dao/AppLogDAO.h"
|
#include "dao/AppLogDAO.h"
|
||||||
#include "db/AppLogEntry.h"
|
#include "db/AppLogEntry.h"
|
||||||
|
#include "net/NetworkService.h"
|
||||||
|
|
||||||
|
NetworkService *AppLogger::s_networkService = nullptr;
|
||||||
|
|
||||||
|
void AppLogger::setNetworkService(NetworkService *service) {
|
||||||
|
s_networkService = service;
|
||||||
|
}
|
||||||
|
|
||||||
void AppLogger::log(const QString &source, const QString &deviceId,
|
void AppLogger::log(const QString &source, const QString &deviceId,
|
||||||
const QString &message, const QByteArray &screenshot) {
|
const QString &message, const QByteArray &screenshot,
|
||||||
|
const QString &event) {
|
||||||
AppLogEntry entry;
|
AppLogEntry entry;
|
||||||
entry.source = source;
|
entry.source = source;
|
||||||
entry.deviceId = deviceId;
|
entry.deviceId = deviceId;
|
||||||
@ -14,4 +23,13 @@ void AppLogger::log(const QString &source, const QString &deviceId,
|
|||||||
entry.screenshot = screenshot;
|
entry.screenshot = screenshot;
|
||||||
entry.timestamp = QDateTime::currentDateTimeUtc();
|
entry.timestamp = QDateTime::currentDateTimeUtc();
|
||||||
AppLogDAO::insert(entry);
|
AppLogDAO::insert(entry);
|
||||||
|
|
||||||
|
if (s_networkService) {
|
||||||
|
QMetaObject::invokeMethod(s_networkService, "postMonitoringLog",
|
||||||
|
Qt::QueuedConnection,
|
||||||
|
Q_ARG(QString, QStringLiteral("ERROR")),
|
||||||
|
Q_ARG(QString, event.isEmpty() ? source : event),
|
||||||
|
Q_ARG(QString, message),
|
||||||
|
Q_ARG(QByteArray, screenshot));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,20 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
|
|
||||||
|
class NetworkService;
|
||||||
|
|
||||||
class AppLogger {
|
class AppLogger {
|
||||||
public:
|
public:
|
||||||
|
static void setNetworkService(NetworkService *service);
|
||||||
|
|
||||||
static void log(
|
static void log(
|
||||||
const QString &source,
|
const QString &source,
|
||||||
const QString &deviceId,
|
const QString &deviceId,
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QByteArray &screenshot = {}
|
const QByteArray &screenshot = {},
|
||||||
|
const QString &event = {}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static NetworkService *s_networkService;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -219,9 +219,26 @@ void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) {
|
|||||||
apiPatch("/api/v1/device/" + device.apiId, body);
|
apiPatch("/api/v1/device/" + device.apiId, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeviceScreener::toggleBankProfilesOnApi(const QString &deviceId, bool enable) {
|
||||||
|
const QList<ApplicationInfo> apps = ApplicationDAO::getApplicationsByDeviceId(deviceId);
|
||||||
|
for (const ApplicationInfo &app : apps) {
|
||||||
|
if (app.bankProfileId.isEmpty()) continue;
|
||||||
|
|
||||||
|
// При включении — только если приложение активно в системе
|
||||||
|
if (enable && app.status != "active") continue;
|
||||||
|
|
||||||
|
const QString enableStr = enable ? "true" : "false";
|
||||||
|
const QString path = "/api/v1/profile/bank_profile/" + app.bankProfileId + "/" + enableStr;
|
||||||
|
qDebug() << "[DeviceScreener] toggleBankProfile:" << app.code << app.bankProfileId << "enable=" << enable;
|
||||||
|
apiPatch(path, QJsonObject());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DeviceScreener::start() {
|
void DeviceScreener::start() {
|
||||||
QMap<QString, bool> bankChecked;
|
|
||||||
while (m_running) {
|
while (m_running) {
|
||||||
|
// Устройства, которые были онлайн в БД до текущего скана
|
||||||
|
const QStringList prevOnline = DeviceDAO::getOnlineDeviceCodes();
|
||||||
|
|
||||||
QList<QPair<QString, QString> > devices = readAdbDevices();
|
QList<QPair<QString, QString> > devices = readAdbDevices();
|
||||||
QStringList onlineIds;
|
QStringList onlineIds;
|
||||||
QList<DeviceInfo> deviceInfos;
|
QList<DeviceInfo> deviceInfos;
|
||||||
@ -231,7 +248,7 @@ void DeviceScreener::start() {
|
|||||||
|
|
||||||
if (status == "device") {
|
if (status == "device") {
|
||||||
onlineIds << device.id;
|
onlineIds << device.id;
|
||||||
const bool isChecked = bankChecked.contains(id);
|
const bool isChecked = prevOnline.contains(id);
|
||||||
device = readDeviceInfo(id, isChecked);
|
device = readDeviceInfo(id, isChecked);
|
||||||
device.image = takeScreenshot(id);
|
device.image = takeScreenshot(id);
|
||||||
if (!device.image.isEmpty()) {
|
if (!device.image.isEmpty()) {
|
||||||
@ -240,9 +257,9 @@ void DeviceScreener::start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isChecked) {
|
if (!isChecked) {
|
||||||
ApplicationDAO::activateAppsForDevice(id);
|
// Новое устройство — включаем на сервере только active профили
|
||||||
|
toggleBankProfilesOnApi(id, true);
|
||||||
}
|
}
|
||||||
bankChecked[id] = true;
|
|
||||||
}
|
}
|
||||||
device.status = status;
|
device.status = status;
|
||||||
device.updateTime = QDateTime::currentDateTime();
|
device.updateTime = QDateTime::currentDateTime();
|
||||||
@ -276,22 +293,14 @@ void DeviceScreener::start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (!deviceInfos.empty()) {
|
|
||||||
// postDevicesData(deviceInfos);
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) {
|
if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) {
|
||||||
qDebug() << "Cant mark devices offline";
|
qDebug() << "Cant mark devices offline";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Деактивировать приложения для устройств, которые ушли в офлайн
|
// Было онлайн в БД, сейчас офлайн — выключаем профили на сервере
|
||||||
QStringList offlineIds;
|
for (const QString &id : prevOnline) {
|
||||||
for (auto it = bankChecked.begin(); it != bankChecked.end();) {
|
if (!onlineIds.contains(id)) {
|
||||||
if (!onlineIds.contains(it.key())) {
|
toggleBankProfilesOnApi(id, false);
|
||||||
ApplicationDAO::deactivateAppsForDevice(it.key());
|
|
||||||
it = bankChecked.erase(it);
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,4 +42,5 @@ private:
|
|||||||
QString createDeviceOnApi(const DeviceInfo &device);
|
QString createDeviceOnApi(const DeviceInfo &device);
|
||||||
void updateDeviceOnApi(const DeviceInfo &device);
|
void updateDeviceOnApi(const DeviceInfo &device);
|
||||||
void postScreenshotToApi(const QString &apiId, const QByteArray &image);
|
void postScreenshotToApi(const QString &apiId, const QByteArray &image);
|
||||||
|
void toggleBankProfilesOnApi(const QString &deviceId, bool enable);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
#include "dao/ApplicationDAO.h"
|
#include "dao/ApplicationDAO.h"
|
||||||
#include "dao/DeviceDAO.h"
|
#include "dao/DeviceDAO.h"
|
||||||
#include "dao/EventDAO.h"
|
#include "dao/EventDAO.h"
|
||||||
|
#include "dao/TransactionDAO.h"
|
||||||
#include "db/ApplicationInfo.h"
|
#include "db/ApplicationInfo.h"
|
||||||
#include "net/NetworkService.h"
|
#include "net/NetworkService.h"
|
||||||
#include "birbank/GetLastDaysHistoryBirbankScript.h"
|
#include "birbank/GetLastDaysHistoryBirbankScript.h"
|
||||||
@ -24,13 +25,14 @@
|
|||||||
#include "ziraat/GetLastDaysHistoryZiraatScript.h"
|
#include "ziraat/GetLastDaysHistoryZiraatScript.h"
|
||||||
#include "ziraat/LoginAndCheckAccountsZiraatScript.h"
|
#include "ziraat/LoginAndCheckAccountsZiraatScript.h"
|
||||||
|
|
||||||
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonObject &data);
|
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data);
|
||||||
|
|
||||||
EventHandler::EventHandler(QObject *parent) : QObject(parent) {}
|
EventHandler::EventHandler(QObject *parent) : QObject(parent) {}
|
||||||
|
|
||||||
EventHandler::~EventHandler() = default;
|
EventHandler::~EventHandler() = default;
|
||||||
|
|
||||||
void EventHandler::start() {
|
void EventHandler::start() {
|
||||||
|
qDebug() << "[EventHandler] start() called, thread:" << QThread::currentThread();
|
||||||
m_network = new NetworkService(this);
|
m_network = new NetworkService(this);
|
||||||
connect(m_network, &NetworkService::tasksReceived, this, &EventHandler::onTasksReceived);
|
connect(m_network, &NetworkService::tasksReceived, this, &EventHandler::onTasksReceived);
|
||||||
|
|
||||||
@ -40,11 +42,17 @@ void EventHandler::start() {
|
|||||||
m_pollTimer->start();
|
m_pollTimer->start();
|
||||||
|
|
||||||
// Первый запрос сразу при старте
|
// Первый запрос сразу при старте
|
||||||
|
qDebug() << "[EventHandler] starting first getTasks()";
|
||||||
m_network->getTasks();
|
m_network->getTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
||||||
if (!m_running) return;
|
if (!m_running) return;
|
||||||
|
if (!tasks.isEmpty())
|
||||||
|
qDebug() << "[EventHandler] onTasksReceived, count:" << tasks.size();
|
||||||
|
|
||||||
|
// Получаем список онлайн устройств один раз
|
||||||
|
const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes();
|
||||||
|
|
||||||
// 1. Сохраняем новые задачи в БД
|
// 1. Сохраняем новые задачи в БД
|
||||||
for (const auto &taskVal : tasks) {
|
for (const auto &taskVal : tasks) {
|
||||||
@ -62,9 +70,6 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пропускаем если уже есть активная запись с таким external_id
|
|
||||||
if (EventDAO::existsActiveByExternalId(materialId)) continue;
|
|
||||||
|
|
||||||
// Ищем аккаунт по material_id напрямую
|
// Ищем аккаунт по material_id напрямую
|
||||||
const AccountInfo account = AccountDAO::findByMaterialId(materialId);
|
const AccountInfo account = AccountDAO::findByMaterialId(materialId);
|
||||||
if (account.id == -1) {
|
if (account.id == -1) {
|
||||||
@ -72,7 +77,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
|||||||
QJsonObject data;
|
QJsonObject data;
|
||||||
data["status"] = "error";
|
data["status"] = "error";
|
||||||
data["description"] = "Account not found for material_id: " + materialId;
|
data["description"] = "Account not found for material_id: " + materialId;
|
||||||
sendTaskResult(typeStr, materialId, bankName, data);
|
sendTaskResult(typeStr, materialId, bankName, QJsonValue(data));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,19 +90,48 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
|||||||
event.bankName = bankName;
|
event.bankName = bankName;
|
||||||
event.timestamp = QDateTime::currentDateTimeUtc();
|
event.timestamp = QDateTime::currentDateTimeUtc();
|
||||||
|
|
||||||
if (EventDAO::insertEvent(event) == -1) {
|
const QJsonObject body = task["body"].toObject();
|
||||||
|
if (eventType == EventType::CreateTransaction) {
|
||||||
|
event.amount = body["amount"].toDouble();
|
||||||
|
event.phone = body["bank_participator_id"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const int eventId = EventDAO::insertEvent(event);
|
||||||
|
if (eventId == -1) {
|
||||||
qCritical() << "[EventHandler] Failed to insert event, external_id:" << materialId;
|
qCritical() << "[EventHandler] Failed to insert event, external_id:" << materialId;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если устройство офлайн — сразу отправляем ошибку на сервер
|
||||||
|
if (!onlineDevices.contains(account.deviceId)) {
|
||||||
|
qWarning() << "[EventHandler] Device offline for material_id:" << materialId << "device:" << account.deviceId;
|
||||||
|
EventDAO::updateEvent(eventId, EventStatus::Error, "Device is offline");
|
||||||
|
QJsonObject errData;
|
||||||
|
errData["status"] = "error";
|
||||||
|
errData["description"] = "Device is offline";
|
||||||
|
sendTaskResult(typeStr, materialId, bankName, QJsonValue(errData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Для каждого онлайн устройства без активного потока — запускаем следующую задачу
|
// 2. Для каждого онлайн устройства без активного потока — запускаем следующую задачу
|
||||||
const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes();
|
|
||||||
for (const QString &deviceId : onlineDevices) {
|
for (const QString &deviceId : onlineDevices) {
|
||||||
if (const QThread *thr = m_threads.value(deviceId, nullptr); thr && thr->isRunning()) continue;
|
if (const QThread *thr = m_threads.value(deviceId, nullptr); thr && thr->isRunning()) continue;
|
||||||
|
|
||||||
const EventInfo event = EventDAO::getFirstWaitEventByDeviceId(deviceId);
|
const EventInfo event = EventDAO::getFirstWaitEventByDeviceId(deviceId);
|
||||||
if (event.id == -1) continue;
|
if (event.id == -1) continue;
|
||||||
|
|
||||||
|
// Если задача ждёт больше 5 минут — отклоняем
|
||||||
|
const qint64 waitSecs = event.timestamp.secsTo(QDateTime::currentDateTimeUtc());
|
||||||
|
if (waitSecs > 5 * 60) {
|
||||||
|
qWarning() << "[EventHandler] Task expired after" << waitSecs << "s, event id:" << event.id;
|
||||||
|
EventDAO::updateEvent(event.id, EventStatus::Error, "Task expired: waited " + QString::number(waitSecs) + "s");
|
||||||
|
QJsonObject errData;
|
||||||
|
errData["status"] = "error";
|
||||||
|
errData["description"] = "Task expired: not started within 5 minutes";
|
||||||
|
sendTaskResult(eventTypeToString(event.type), event.externalId, event.bankName, QJsonValue(errData));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const AccountInfo account = AccountDAO::getAccountById(event.accountId);
|
const AccountInfo account = AccountDAO::getAccountById(event.accountId);
|
||||||
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, account.appCode);
|
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, account.appCode);
|
||||||
if (app.status != "active") continue;
|
if (app.status != "active") continue;
|
||||||
@ -112,7 +146,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName,
|
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName,
|
||||||
const QJsonObject &data) {
|
const QJsonValue &data) {
|
||||||
QJsonObject obj;
|
QJsonObject obj;
|
||||||
obj["type"] = type;
|
obj["type"] = type;
|
||||||
obj["material_id"] = materialId; // UUID string
|
obj["material_id"] = materialId; // UUID string
|
||||||
@ -156,12 +190,57 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
if (!EventDAO::updateEvent(event.id, newStatus, result)) {
|
if (!EventDAO::updateEvent(event.id, newStatus, result)) {
|
||||||
qCritical() << "[EventHandler] Failed to update event:" << event.id;
|
qCritical() << "[EventHandler] Failed to update event:" << event.id;
|
||||||
} else {
|
} else {
|
||||||
sendEventStatus(
|
QJsonValue taskData;
|
||||||
event.deviceId,
|
if (newStatus == EventStatus::Error) {
|
||||||
|
QJsonObject errObj;
|
||||||
|
errObj["status"] = "error";
|
||||||
|
errObj["description"] = result;
|
||||||
|
taskData = errObj;
|
||||||
|
} else if (event.type == EventType::FetchProfile) {
|
||||||
|
const AccountInfo account = AccountDAO::getAccountById(event.accountId);
|
||||||
|
EventDAO::updateEventAmount(event.id, account.amount);
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["bank_account_id"] = QString();
|
||||||
|
obj["amount"] = static_cast<int>(account.amount);
|
||||||
|
obj["currency"] = 643; // RUB
|
||||||
|
taskData = obj;
|
||||||
|
} else if (event.type == EventType::FetchTransactions) {
|
||||||
|
QList<TransactionInfo> txList = TransactionDAO::getTransactionsWithinHours(event.accountId, 24);
|
||||||
|
if (txList.isEmpty()) {
|
||||||
|
txList = TransactionDAO::getLastTransactions(event.accountId, 10);
|
||||||
|
}
|
||||||
|
QJsonArray arr;
|
||||||
|
for (const auto &tx : txList) {
|
||||||
|
QJsonObject txObj;
|
||||||
|
txObj["bank_transaction_id"] = tx.bankTrExternalId.isEmpty() ? QJsonValue(QJsonValue::Null) : QJsonValue(tx.bankTrExternalId);
|
||||||
|
txObj["amount"] = static_cast<int>(tx.amount);
|
||||||
|
txObj["currency"] = 643; // RUB
|
||||||
|
txObj["transaction_type"] = (tx.type == TransactionType::Phone) ? "top-up" : "bank";
|
||||||
|
txObj["bank_participator_id"]= tx.phone;
|
||||||
|
txObj["status"] = tx.status == TransactionStatus::Complete ? "completed" : (tx.status == TransactionStatus::Wait ? "progress" : "completed");
|
||||||
|
txObj["extra"] = QJsonObject();
|
||||||
|
if (tx.timestamp.isValid()) {
|
||||||
|
txObj["created_at"] = tx.timestamp.toString(Qt::ISODate);
|
||||||
|
} else if (!tx.bankTime.isEmpty()) {
|
||||||
|
QDateTime dt = QDateTime::fromString(tx.bankTime, "dd.MM.yyyy, HH:mm");
|
||||||
|
if (!dt.isValid()) dt = QDateTime::fromString(tx.bankTime, "dd.MM.yyyy HH:mm");
|
||||||
|
txObj["created_at"] = dt.isValid() ? dt.toString(Qt::ISODate) : QJsonValue(QJsonValue::Null);
|
||||||
|
} else {
|
||||||
|
txObj["created_at"] = QJsonValue(QJsonValue::Null);
|
||||||
|
}
|
||||||
|
arr.append(txObj);
|
||||||
|
}
|
||||||
|
taskData = arr;
|
||||||
|
} else {
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["status"] = "completed";
|
||||||
|
taskData = obj;
|
||||||
|
}
|
||||||
|
sendTaskResult(
|
||||||
|
eventTypeToString(event.type),
|
||||||
event.externalId,
|
event.externalId,
|
||||||
event.id,
|
event.bankName,
|
||||||
eventStatusToString(newStatus),
|
taskData
|
||||||
result
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -35,6 +35,7 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) {
|
|||||||
m_pathMaterial = settings.value("net/pathMaterial", "/api/v1/profile/material").toString();
|
m_pathMaterial = settings.value("net/pathMaterial", "/api/v1/profile/material").toString();
|
||||||
m_pathGetTasks = settings.value("net/pathGetTasks", "/api/v1/task/get_tasks").toString();
|
m_pathGetTasks = settings.value("net/pathGetTasks", "/api/v1/task/get_tasks").toString();
|
||||||
m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString();
|
m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString();
|
||||||
|
m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString();
|
||||||
m_manager = new QNetworkAccessManager(this);
|
m_manager = new QNetworkAccessManager(this);
|
||||||
m_token = settings.value("common/token").toString();
|
m_token = settings.value("common/token").toString();
|
||||||
m_accessToken = settings.value("common/access_token").toString();
|
m_accessToken = settings.value("common/access_token").toString();
|
||||||
@ -412,6 +413,26 @@ void NetworkService::patchBankProfile() {
|
|||||||
emit finished();
|
emit finished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkService::toggleBankProfile() {
|
||||||
|
if (!ensureValidToken()) { emit finished(); return; }
|
||||||
|
|
||||||
|
const QString bankProfileId = m_extraData.value("bank_profile_id").toString();
|
||||||
|
const bool enable = m_extraData.value("enable").toBool();
|
||||||
|
|
||||||
|
if (bankProfileId.isEmpty()) {
|
||||||
|
qWarning() << "[NetworkService] toggleBankProfile: bank_profile_id is empty";
|
||||||
|
emit finished();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString enableStr = enable ? "true" : "false";
|
||||||
|
const QString path = m_apiBase + m_pathBankProfile + "/" + bankProfileId + "/" + enableStr;
|
||||||
|
patchJson(path, QJsonObject(), true);
|
||||||
|
qDebug() << "[NetworkService] toggleBankProfile done:" << bankProfileId << "enable=" << enable;
|
||||||
|
|
||||||
|
emit finished();
|
||||||
|
}
|
||||||
|
|
||||||
void NetworkService::postMaterial() {
|
void NetworkService::postMaterial() {
|
||||||
if (!ensureValidToken()) { emit finished(); return; }
|
if (!ensureValidToken()) { emit finished(); return; }
|
||||||
|
|
||||||
@ -594,10 +615,21 @@ void NetworkService::getTasks() {
|
|||||||
if (m_fetchingTasks) return;
|
if (m_fetchingTasks) return;
|
||||||
m_fetchingTasks = true;
|
m_fetchingTasks = true;
|
||||||
|
|
||||||
|
if (!ensureValidToken()) {
|
||||||
|
m_fetchingTasks = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const QSettings settings("config.ini", QSettings::IniFormat);
|
const QSettings settings("config.ini", QSettings::IniFormat);
|
||||||
const QString accessToken = settings.value("common/access_token").toString();
|
const QString accessToken = settings.value("common/access_token").toString();
|
||||||
|
|
||||||
QUrl url(m_apiBase + m_pathGetTasks);
|
QUrl url(m_apiBase + m_pathGetTasks);
|
||||||
|
QUrlQuery urlQuery;
|
||||||
|
const QStringList materialIds = AccountDAO::getActiveMaterialIds();
|
||||||
|
for (const QString &mid : materialIds) {
|
||||||
|
urlQuery.addQueryItem("materials_id", mid);
|
||||||
|
}
|
||||||
|
url.setQuery(urlQuery);
|
||||||
QNetworkRequest request(url);
|
QNetworkRequest request(url);
|
||||||
request.setRawHeader("Authorization", ("Bearer " + accessToken).toUtf8());
|
request.setRawHeader("Authorization", ("Bearer " + accessToken).toUtf8());
|
||||||
|
|
||||||
@ -607,7 +639,8 @@ void NetworkService::getTasks() {
|
|||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
|
|
||||||
if (reply->error() != QNetworkReply::NoError) {
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
qWarning() << "[NetworkService] getTasks error:" << reply->errorString();
|
qWarning() << "[NetworkService] getTasks error:" << reply->errorString()
|
||||||
|
<< "status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -638,6 +671,37 @@ void NetworkService::postTaskResult() {
|
|||||||
emit finished();
|
emit finished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkService::postMonitoringLog(const QString &type, const QString &event,
|
||||||
|
const QString &message, const QByteArray &image) {
|
||||||
|
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
|
|
||||||
|
addFormField(multiPart, "type", type);
|
||||||
|
addFormField(multiPart, "event", event);
|
||||||
|
addFormField(multiPart, "message", message);
|
||||||
|
|
||||||
|
QHttpPart imagePart;
|
||||||
|
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
|
||||||
|
imagePart.setHeader(
|
||||||
|
QNetworkRequest::ContentDispositionHeader,
|
||||||
|
QStringLiteral("form-data; name=\"image\"; filename=\"screenshot.png\"")
|
||||||
|
);
|
||||||
|
imagePart.setBody(image.isEmpty() ? QByteArray(1, '\0') : image);
|
||||||
|
multiPart->append(imagePart);
|
||||||
|
|
||||||
|
const QUrl url(m_apiBase + m_pathMonitoringLog);
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||||
|
multiPart->setParent(reply);
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, [reply]() {
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
qWarning() << "[NetworkService] postMonitoringLog failed:" << reply->errorString();
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void NetworkService::start() {
|
void NetworkService::start() {
|
||||||
m_running = true;
|
m_running = true;
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,8 @@ public slots:
|
|||||||
|
|
||||||
void patchBankProfile();
|
void patchBankProfile();
|
||||||
|
|
||||||
|
void toggleBankProfile();
|
||||||
|
|
||||||
void postMaterial();
|
void postMaterial();
|
||||||
|
|
||||||
void postDeviceScreenshot();
|
void postDeviceScreenshot();
|
||||||
@ -52,6 +54,9 @@ public slots:
|
|||||||
|
|
||||||
void postTaskResult();
|
void postTaskResult();
|
||||||
|
|
||||||
|
void postMonitoringLog(const QString &type, const QString &event,
|
||||||
|
const QString &message, const QByteArray &image);
|
||||||
|
|
||||||
void start();
|
void start();
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
@ -86,6 +91,7 @@ private:
|
|||||||
QString m_pathMaterial;
|
QString m_pathMaterial;
|
||||||
QString m_pathGetTasks;
|
QString m_pathGetTasks;
|
||||||
QString m_pathTaskResult;
|
QString m_pathTaskResult;
|
||||||
|
QString m_pathMonitoringLog;
|
||||||
bool m_fetchingTasks = false;
|
bool m_fetchingTasks = false;
|
||||||
|
|
||||||
QNetworkAccessManager *m_manager;
|
QNetworkAccessManager *m_manager;
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "TutorialWindow.h"
|
#include "TutorialWindow.h"
|
||||||
#include "bank/BankListWindow.h"
|
#include "bank/BankListWindow.h"
|
||||||
|
#include "event/EventWindow.h"
|
||||||
#include "log/LogWindow.h"
|
#include "log/LogWindow.h"
|
||||||
#include "db/DeviceInfo.h"
|
#include "db/DeviceInfo.h"
|
||||||
#include "device/DeviceWidget.h"
|
#include "device/DeviceWidget.h"
|
||||||
@ -39,10 +40,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
|||||||
// auto *accountPageAction = new QAction("Аккаунты", this);
|
// auto *accountPageAction = new QAction("Аккаунты", this);
|
||||||
auto *bankListAction = new QAction("Банки", this);
|
auto *bankListAction = new QAction("Банки", this);
|
||||||
auto *tutorialPageAction = new QAction("Инструкция", this);
|
auto *tutorialPageAction = new QAction("Инструкция", this);
|
||||||
|
auto *eventAction = new QAction("События", this);
|
||||||
auto *logAction = new QAction("Логи", this);
|
auto *logAction = new QAction("Логи", this);
|
||||||
menu->addAction(devicesPageAction);
|
menu->addAction(devicesPageAction);
|
||||||
// menu->addAction(accountPageAction);
|
// menu->addAction(accountPageAction);
|
||||||
menu->addAction(bankListAction);
|
menu->addAction(bankListAction);
|
||||||
|
menu->addAction(eventAction);
|
||||||
menu->addAction(tutorialPageAction);
|
menu->addAction(tutorialPageAction);
|
||||||
menu->addAction(logAction);
|
menu->addAction(logAction);
|
||||||
|
|
||||||
@ -76,6 +79,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
|||||||
m_tutorialWindow->activateWindow();
|
m_tutorialWindow->activateWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(eventAction, &QAction::triggered, this, [=]() {
|
||||||
|
if (!m_eventWindow) {
|
||||||
|
m_eventWindow = new EventWindow(this);
|
||||||
|
}
|
||||||
|
m_eventWindow->show();
|
||||||
|
m_eventWindow->raise();
|
||||||
|
m_eventWindow->activateWindow();
|
||||||
|
});
|
||||||
|
|
||||||
connect(logAction, &QAction::triggered, this, [=]() {
|
connect(logAction, &QAction::triggered, this, [=]() {
|
||||||
if (!m_logWindow) {
|
if (!m_logWindow) {
|
||||||
m_logWindow = new LogWindow(this);
|
m_logWindow = new LogWindow(this);
|
||||||
|
|||||||
@ -28,6 +28,7 @@ private:
|
|||||||
QWidget *m_tutorialWindow = nullptr;
|
QWidget *m_tutorialWindow = nullptr;
|
||||||
QWidget *m_bankListWindow = nullptr;
|
QWidget *m_bankListWindow = nullptr;
|
||||||
QWidget *m_logWindow = nullptr;
|
QWidget *m_logWindow = nullptr;
|
||||||
|
QWidget *m_eventWindow = nullptr;
|
||||||
|
|
||||||
void createDevicePage();
|
void createDevicePage();
|
||||||
|
|
||||||
|
|||||||
@ -359,8 +359,23 @@ void AccountSettingsWindow::startGetLastTransactions(const QString &appCode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) {
|
void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) {
|
||||||
Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status)
|
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code);
|
||||||
// TODO: заменить на новый API
|
if (app.bankProfileId.isEmpty()) {
|
||||||
|
qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << code;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool enable = (status == "active");
|
||||||
|
auto *thread = new QThread;
|
||||||
|
auto *net = new NetworkService;
|
||||||
|
net->moveToThread(thread);
|
||||||
|
net->addExtra("bank_profile_id", app.bankProfileId);
|
||||||
|
net->addExtra("enable", enable);
|
||||||
|
connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
||||||
|
connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||||||
|
connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||||||
|
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
thread->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AccountSettingsWindow::updateAccountData(const AccountInfo &account) {
|
void AccountSettingsWindow::updateAccountData(const AccountInfo &account) {
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#include "dao/ApplicationDAO.h"
|
#include "dao/ApplicationDAO.h"
|
||||||
#include "db/ApplicationInfo.h"
|
#include "db/ApplicationInfo.h"
|
||||||
|
#include "net/NetworkService.h"
|
||||||
#include "common/PaddedItemDelegate.h"
|
#include "common/PaddedItemDelegate.h"
|
||||||
#include "device/EditBankDialog.h"
|
#include "device/EditBankDialog.h"
|
||||||
#include "birbank/LoginAndCheckAccountsBirbankScript.h"
|
#include "birbank/LoginAndCheckAccountsBirbankScript.h"
|
||||||
@ -95,8 +96,23 @@ void BankSettingsWindow::startCheckPinCode(const QString &appCode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sendAppStatus(const QString &deviceId, const QString &code, const QString &status) {
|
void sendAppStatus(const QString &deviceId, const QString &code, const QString &status) {
|
||||||
Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status)
|
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code);
|
||||||
// TODO: заменить на новый API
|
if (app.bankProfileId.isEmpty()) {
|
||||||
|
qWarning() << "[BankSettings] sendAppStatus: no bankProfileId for" << code;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool enable = (status == "active");
|
||||||
|
auto *thread = new QThread;
|
||||||
|
auto *net = new NetworkService;
|
||||||
|
net->moveToThread(thread);
|
||||||
|
net->addExtra("bank_profile_id", app.bankProfileId);
|
||||||
|
net->addExtra("enable", enable);
|
||||||
|
QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
||||||
|
QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||||||
|
QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||||||
|
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
thread->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
|
void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
|
||||||
|
|||||||
181
views/event/EventWindow.cpp
Normal file
181
views/event/EventWindow.cpp
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
#include "EventWindow.h"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QThread>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "dao/EventDAO.h"
|
||||||
|
#include "net/NetworkService.h"
|
||||||
|
|
||||||
|
static constexpr int COL_ID = 0;
|
||||||
|
static constexpr int COL_TIME = 1;
|
||||||
|
static constexpr int COL_TYPE = 2;
|
||||||
|
static constexpr int COL_BANK = 3;
|
||||||
|
static constexpr int COL_DEVICE = 4;
|
||||||
|
static constexpr int COL_STATUS = 5;
|
||||||
|
static constexpr int COL_AMOUNT = 6;
|
||||||
|
static constexpr int COL_COMMENT = 7;
|
||||||
|
static constexpr int COL_ACTION = 8;
|
||||||
|
|
||||||
|
static void sendErrorToServer(const QString &type, const QString &materialId, const QString &bankName) {
|
||||||
|
QJsonObject errData;
|
||||||
|
errData["status"] = "error";
|
||||||
|
errData["description"] = "Closed manually";
|
||||||
|
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["type"] = type;
|
||||||
|
obj["material_id"] = materialId;
|
||||||
|
obj["bank_name"] = bankName;
|
||||||
|
obj["data"] = errData;
|
||||||
|
|
||||||
|
auto *thread = new QThread;
|
||||||
|
auto *networkService = new NetworkService;
|
||||||
|
networkService->moveToThread(thread);
|
||||||
|
networkService->setPayload(QJsonDocument(obj).toJson());
|
||||||
|
QObject::connect(thread, &QThread::started, networkService, &NetworkService::postTaskResult);
|
||||||
|
QObject::connect(networkService, &NetworkService::finished, thread, &QThread::quit);
|
||||||
|
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
|
||||||
|
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString statusDisplayName(EventStatus s) {
|
||||||
|
switch (s) {
|
||||||
|
case EventStatus::Wait: return "Ожидание";
|
||||||
|
case EventStatus::InProgress: return "В работе";
|
||||||
|
case EventStatus::Complete: return "Завершено";
|
||||||
|
case EventStatus::Error: return "Ошибка";
|
||||||
|
default: return "—";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString typeDisplayName(EventType t) {
|
||||||
|
switch (t) {
|
||||||
|
case EventType::FetchProfile: return "Профиль";
|
||||||
|
case EventType::FetchTransactions: return "Транзакции";
|
||||||
|
case EventType::CreateTransaction: return "Платёж";
|
||||||
|
default: return "—";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
|
||||||
|
setWindowTitle("События");
|
||||||
|
resize(1100, 600);
|
||||||
|
|
||||||
|
// ── Таблица ───────────────────────────────────────────────────────────────
|
||||||
|
m_table = new QTableWidget(this);
|
||||||
|
m_table->setColumnCount(9);
|
||||||
|
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_TYPE, 100);
|
||||||
|
m_table->setColumnWidth(COL_BANK, 90);
|
||||||
|
m_table->setColumnWidth(COL_DEVICE, 120);
|
||||||
|
m_table->setColumnWidth(COL_STATUS, 100);
|
||||||
|
m_table->setColumnWidth(COL_AMOUNT, 80);
|
||||||
|
m_table->setColumnWidth(COL_ACTION, 80);
|
||||||
|
|
||||||
|
// ── Пагинация ─────────────────────────────────────────────────────────────
|
||||||
|
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(m_table, 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
loadPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventWindow::loadPage() {
|
||||||
|
const int total = EventDAO::getTotalCount();
|
||||||
|
m_totalPages = qMax(1, (total + m_pageSize - 1) / m_pageSize);
|
||||||
|
m_page = qBound(0, m_page, m_totalPages - 1);
|
||||||
|
|
||||||
|
const QList<EventInfo> events = EventDAO::getEvents(m_page, m_pageSize);
|
||||||
|
|
||||||
|
m_table->setRowCount(static_cast<int>(events.size()));
|
||||||
|
for (int i = 0; i < events.size(); ++i) {
|
||||||
|
const auto &e = events[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_TYPE, new QTableWidgetItem(typeDisplayName(e.type)));
|
||||||
|
m_table->setItem(i, COL_BANK, new QTableWidgetItem(e.bankName));
|
||||||
|
m_table->setItem(i, COL_DEVICE, new QTableWidgetItem(e.deviceId));
|
||||||
|
m_table->setItem(i, COL_STATUS, new QTableWidgetItem(statusDisplayName(e.status)));
|
||||||
|
m_table->setItem(i, COL_AMOUNT, new QTableWidgetItem(
|
||||||
|
e.amount > 0 ? QString::number(e.amount, 'f', 2) : "—"));
|
||||||
|
m_table->setItem(i, COL_COMMENT, new QTableWidgetItem(e.comment));
|
||||||
|
|
||||||
|
// Подсветка строки по статусу
|
||||||
|
QColor bg;
|
||||||
|
switch (e.status) {
|
||||||
|
case EventStatus::Error: bg = QColor(255, 230, 230); break;
|
||||||
|
case EventStatus::Complete: bg = QColor(230, 255, 230); break;
|
||||||
|
case EventStatus::InProgress: bg = QColor(230, 240, 255); break;
|
||||||
|
default: bg = QColor(255, 255, 230); break;
|
||||||
|
}
|
||||||
|
for (int col = 0; col < m_table->columnCount(); ++col) {
|
||||||
|
if (auto *item = m_table->item(i, col))
|
||||||
|
item->setBackground(bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка "Закрыть" только для активных задач
|
||||||
|
if (e.status == EventStatus::Wait || e.status == EventStatus::InProgress) {
|
||||||
|
auto *closeBtn = new QPushButton("Закрыть");
|
||||||
|
closeBtn->setStyleSheet("background: #e74c3c; color: white; border: none; padding: 3px 8px; border-radius: 3px;");
|
||||||
|
const int eventId = e.id;
|
||||||
|
const QString externalId = e.externalId;
|
||||||
|
const QString bankName = e.bankName;
|
||||||
|
const QString typeStr = eventTypeToString(e.type);
|
||||||
|
connect(closeBtn, &QPushButton::clicked, this, [this, eventId, externalId, bankName, typeStr]() {
|
||||||
|
EventDAO::updateEvent(eventId, EventStatus::Error, "Closed manually");
|
||||||
|
sendErrorToServer(typeStr, externalId, bankName);
|
||||||
|
loadPage();
|
||||||
|
});
|
||||||
|
m_table->setCellWidget(i, COL_ACTION, closeBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventWindow::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);
|
||||||
|
}
|
||||||
25
views/event/EventWindow.h
Normal file
25
views/event/EventWindow.h
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QTableWidget>
|
||||||
|
|
||||||
|
class EventWindow final : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit EventWindow(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QTableWidget *m_table;
|
||||||
|
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();
|
||||||
|
};
|
||||||
@ -3,6 +3,7 @@
|
|||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
#include <QResizeEvent>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QSplitter>
|
#include <QSplitter>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@ -127,16 +128,27 @@ void LogWindow::updatePagination() {
|
|||||||
void LogWindow::showScreenshot(int logId) {
|
void LogWindow::showScreenshot(int logId) {
|
||||||
const QByteArray bytes = AppLogDAO::getScreenshot(logId);
|
const QByteArray bytes = AppLogDAO::getScreenshot(logId);
|
||||||
if (bytes.isEmpty()) {
|
if (bytes.isEmpty()) {
|
||||||
|
m_originalPixmap = {};
|
||||||
m_screenshotLabel->setPixmap({});
|
m_screenshotLabel->setPixmap({});
|
||||||
m_screenshotLabel->setText("Скриншот отсутствует");
|
m_screenshotLabel->setText("Скриншот отсутствует");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QPixmap px;
|
if (!m_originalPixmap.loadFromData(bytes)) {
|
||||||
if (!px.loadFromData(bytes)) {
|
m_originalPixmap = {};
|
||||||
m_screenshotLabel->setText("Не удалось загрузить скриншот");
|
m_screenshotLabel->setText("Не удалось загрузить скриншот");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
rescaleScreenshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogWindow::rescaleScreenshot() {
|
||||||
|
if (m_originalPixmap.isNull()) return;
|
||||||
m_screenshotLabel->setPixmap(
|
m_screenshotLabel->setPixmap(
|
||||||
px.scaled(m_screenshotLabel->width(), 9999, Qt::KeepAspectRatio, Qt::SmoothTransformation)
|
m_originalPixmap.scaled(m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LogWindow::resizeEvent(QResizeEvent *event) {
|
||||||
|
QDialog::resizeEvent(event);
|
||||||
|
rescaleScreenshot();
|
||||||
|
}
|
||||||
|
|||||||
@ -21,7 +21,13 @@ private:
|
|||||||
int m_pageSize = 50;
|
int m_pageSize = 50;
|
||||||
int m_totalPages = 1;
|
int m_totalPages = 1;
|
||||||
|
|
||||||
|
QPixmap m_originalPixmap;
|
||||||
|
|
||||||
void loadPage();
|
void loadPage();
|
||||||
void updatePagination();
|
void updatePagination();
|
||||||
void showScreenshot(int logId);
|
void showScreenshot(int logId);
|
||||||
|
void rescaleScreenshot();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user