1
0
forked from BRT/arc

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:
slava 2026-03-04 22:56:47 +07:00
parent 73e23f437d
commit 832fb03aac
30 changed files with 736 additions and 50 deletions

View File

@ -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 ()

View File

@ -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;

View File

@ -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";

View File

@ -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;
} }

View File

@ -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"};

View File

@ -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]

View File

@ -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.

View File

@ -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());

View File

@ -31,4 +31,6 @@ public:
); );
[[nodiscard]] static AccountInfo findByMaterialId(const QString &materialId); [[nodiscard]] static AccountInfo findByMaterialId(const QString &materialId);
[[nodiscard]] static QStringList getActiveMaterialIds();
}; };

View File

@ -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"(

View File

@ -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();
}; };

View File

@ -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");

View File

@ -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

View File

@ -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");

View File

@ -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));
}
} }

View File

@ -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;
}; };

View File

@ -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;
} }
} }

View File

@ -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);
}; };

View File

@ -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
); );
} }

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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();

View File

@ -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) {

View File

@ -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
View 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
View 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();
};

View File

@ -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();
}

View File

@ -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;
}; };