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)
|
||||
# Укажи путь к Qt6
|
||||
set(CMAKE_PREFIX_PATH "/opt/homebrew/opt/qt")
|
||||
include_directories("/opt/homebrew/include")
|
||||
link_directories("/opt/homebrew/lib")
|
||||
endif ()
|
||||
|
||||
@ -135,6 +136,8 @@ elseif (APPLE)
|
||||
Qt6::Sql
|
||||
Qt6::Xml
|
||||
Qt6::Network
|
||||
tesseract
|
||||
leptonica
|
||||
)
|
||||
endif ()
|
||||
|
||||
|
||||
@ -61,7 +61,8 @@ void GetCardInfoScript::doStart() {
|
||||
m_error = "Cannot reach home screen: " + m_deviceId;
|
||||
qWarning() << 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);
|
||||
emit finishedWithResult(m_error);
|
||||
return;
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
#include "GetLastTransactionsScript.h"
|
||||
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QThread>
|
||||
#include <QTimeZone>
|
||||
|
||||
#include <tesseract/baseapi.h>
|
||||
#include <leptonica/allheaders.h>
|
||||
|
||||
#include "adb/AdbUtils.h"
|
||||
#include "dao/AccountDAO.h"
|
||||
#include "dao/ApplicationDAO.h"
|
||||
@ -12,6 +16,46 @@
|
||||
#include "time/DateUtils.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 {
|
||||
|
||||
GetLastTransactionsScript::GetLastTransactionsScript(
|
||||
@ -55,7 +99,8 @@ void GetLastTransactionsScript::doStart() {
|
||||
m_error = "Cannot reach home screen: " + m_deviceId;
|
||||
qWarning() << 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);
|
||||
emit finishedWithResult(m_error);
|
||||
return;
|
||||
@ -259,12 +304,28 @@ void GetLastTransactionsScript::doStart() {
|
||||
screencapProc.waitForFinished(5000);
|
||||
const QByteArray screenshot = screencapProc.readAllStandardOutput();
|
||||
if (!screenshot.isEmpty()) {
|
||||
// Сохраняем в БД
|
||||
TransactionInfo saved = TransactionDAO::findByBankTimeAndAmount(
|
||||
accountId, tx.bankName, tx.bankTime, tx.amount);
|
||||
if (saved.id != -1) {
|
||||
TransactionDAO::updateReceiptImage(saved.id, screenshot);
|
||||
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 {
|
||||
qWarning() << "[GetLastTransactions] Screenshot is empty";
|
||||
|
||||
@ -40,7 +40,8 @@ void LoginAndCheckAccountsScript::doStart() {
|
||||
m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode;
|
||||
qWarning() << m_error;
|
||||
AppLogger::log("ozon/LoginAndCheck", m_deviceId, m_error,
|
||||
AdbUtils::captureScreenshotBytes(m_deviceId));
|
||||
AdbUtils::captureScreenshotBytes(m_deviceId),
|
||||
"FETCH_PROFILE");
|
||||
if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) {
|
||||
qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode;
|
||||
}
|
||||
|
||||
@ -439,10 +439,20 @@ QList<Node> ScreenXmlParser::findPinCodeNodes(const QString &pinCode) {
|
||||
// 6. PIN-сетка — строки ровно с 3 узлами (3 столбца)
|
||||
// Строки 1-3: цифры 1-9
|
||||
// Строка 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;
|
||||
for (const QList<Node> &row: rows) {
|
||||
if (row.size() == 3) gridRows.append(row);
|
||||
}
|
||||
qDebug() << "[findPinCodeNodes] gridRows:" << gridRows.size() << "pinCode len:" << pinCode.size();
|
||||
|
||||
QMap<QString, Node> digitMap;
|
||||
const QStringList digits19 = {"1", "2", "3", "4", "5", "6", "7", "8", "9"};
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
list=ozon
|
||||
|
||||
[common]
|
||||
access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyNDI3Njk5fQ.OYKZQ6znq88jCyKhHVFjfowkpYTMHlDNP8shh4nGxJE
|
||||
access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyNzE0MTQyfQ.-074b8eEWeLgzoxxpq5o54G1npivuC_VWvu4NLEz_gY
|
||||
api_key=0eee77b62013253e02e1a69bc6a505bb37757fb7
|
||||
currencies=USD, KRW, AZN, RUB
|
||||
desktop_id=b86eac8e-dc89-4a9e-a5e8-7f85db011391
|
||||
profile_id=c4552156-d179-4a79-9e91-587038c95cf0
|
||||
refresh_token=f9016b28-4dd5-429f-868e-8a8e0b246498
|
||||
token_expires_at=1772427699
|
||||
refresh_token=def3036b-b62d-4550-8971-94d7ef57eba2
|
||||
token_expires_at=1772714142
|
||||
version=0.0.1
|
||||
|
||||
[db]
|
||||
|
||||
@ -12,9 +12,17 @@ DatabaseManager::DatabaseManager() = default;
|
||||
DatabaseManager::~DatabaseManager() {
|
||||
QMutexLocker locker(&mutex);
|
||||
|
||||
// QSqlDatabase требует живого QCoreApplication — при завершении
|
||||
// статический синглтон может разрушаться после QApplication
|
||||
if (!QCoreApplication::instance()) {
|
||||
connectionPool.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = connectionPool.begin(); it != connectionPool.end(); ++it) {
|
||||
QSqlDatabase::removeDatabase(it.key());
|
||||
}
|
||||
connectionPool.clear();
|
||||
qDebug() << "Все соединения закрыты";
|
||||
}
|
||||
|
||||
@ -320,6 +328,39 @@ void DatabaseManager::initSchema() {
|
||||
q.exec("PRAGMA user_version = 10");
|
||||
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() {
|
||||
|
||||
Binary file not shown.
@ -217,6 +217,28 @@ AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &a
|
||||
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) {
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
|
||||
|
||||
@ -31,4 +31,6 @@ public:
|
||||
);
|
||||
|
||||
[[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.phone = query.value("phone").toString();
|
||||
event.bankName = query.value("bank_name").toString();
|
||||
event.comment = query.value("comment").toString();
|
||||
event.timestamp = DateUtils::fromUtcString(query.value("timestamp").toString());
|
||||
event.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());
|
||||
event.completeTime = DateUtils::fromUtcString(query.value("complete_time").toString());
|
||||
@ -101,6 +102,18 @@ bool EventDAO::updateEvent(
|
||||
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> events;
|
||||
|
||||
@ -127,7 +140,8 @@ QList<EventInfo> EventDAO::getAllEvents(const EventStatus &status) {
|
||||
EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) {
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
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
|
||||
WHERE status = :status AND device_id = :device_id
|
||||
ORDER BY timestamp ASC
|
||||
@ -147,6 +161,37 @@ EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) {
|
||||
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) {
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
query.prepare(R"(
|
||||
|
||||
@ -17,4 +17,10 @@ public:
|
||||
[[nodiscard]] static EventInfo getFirstWaitEventByDeviceId(const QString &key);
|
||||
|
||||
[[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;
|
||||
}
|
||||
|
||||
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(
|
||||
const int id,
|
||||
const TransactionStatus status,
|
||||
@ -348,6 +371,18 @@ bool TransactionDAO::updateExternalTransactionId(const int id, const int externa
|
||||
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) {
|
||||
QSqlQuery query(DatabaseManager::instance().database());
|
||||
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> getLastTransactions(int accountId, int limit);
|
||||
|
||||
[[nodiscard]] static bool updateTransaction(
|
||||
int id, TransactionStatus status,
|
||||
const QString &oldDesc,
|
||||
@ -27,6 +29,8 @@ public:
|
||||
|
||||
[[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);
|
||||
|
||||
// Найти транзакцию по bank_name + bank_time + amount
|
||||
|
||||
8
main.cpp
8
main.cpp
@ -16,6 +16,7 @@
|
||||
#include <QFile>
|
||||
#include <QSettings>
|
||||
|
||||
#include "AppLogger.h"
|
||||
#include "EventHandler.h"
|
||||
#include "net/NetworkService.h"
|
||||
#include "ozon/PayByPhoneScript.h"
|
||||
@ -100,6 +101,7 @@ void startPayByPhoneTest() {
|
||||
void startNetworkService(const QCoreApplication &app) {
|
||||
auto *paymentThread = new QThread;
|
||||
auto *networkService = new NetworkService;
|
||||
AppLogger::setNetworkService(networkService);
|
||||
networkService->moveToThread(paymentThread);
|
||||
QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::start);
|
||||
QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit);
|
||||
@ -209,7 +211,7 @@ int main(int argc, char *argv[]) {
|
||||
startNetworkService(app);
|
||||
startDeviceScreener(app);
|
||||
startEventHandler(app);
|
||||
startPayByPhoneTest();
|
||||
// startPayByPhoneTest();
|
||||
}, Qt::QueuedConnection);
|
||||
};
|
||||
|
||||
@ -237,7 +239,7 @@ int main(int argc, char *argv[]) {
|
||||
startNetworkService(app);
|
||||
startDeviceScreener(app);
|
||||
startEventHandler(app);
|
||||
startPayByPhoneTest();
|
||||
// startPayByPhoneTest();
|
||||
} else if (r == 0) {
|
||||
QSettings s("config.ini", QSettings::IniFormat);
|
||||
s.remove("common/api_key");
|
||||
@ -258,7 +260,7 @@ int main(int argc, char *argv[]) {
|
||||
startNetworkService(app);
|
||||
startDeviceScreener(app);
|
||||
startEventHandler(app);
|
||||
startPayByPhoneTest();
|
||||
// startPayByPhoneTest();
|
||||
} else if (result == 0) {
|
||||
QSettings s("config.ini", QSettings::IniFormat);
|
||||
s.remove("common/api_key");
|
||||
|
||||
@ -1,12 +1,21 @@
|
||||
#include "AppLogger.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QMetaObject>
|
||||
|
||||
#include "dao/AppLogDAO.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,
|
||||
const QString &message, const QByteArray &screenshot) {
|
||||
const QString &message, const QByteArray &screenshot,
|
||||
const QString &event) {
|
||||
AppLogEntry entry;
|
||||
entry.source = source;
|
||||
entry.deviceId = deviceId;
|
||||
@ -14,4 +23,13 @@ void AppLogger::log(const QString &source, const QString &deviceId,
|
||||
entry.screenshot = screenshot;
|
||||
entry.timestamp = QDateTime::currentDateTimeUtc();
|
||||
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 <QByteArray>
|
||||
|
||||
class NetworkService;
|
||||
|
||||
class AppLogger {
|
||||
public:
|
||||
static void setNetworkService(NetworkService *service);
|
||||
|
||||
static void log(
|
||||
const QString &source,
|
||||
const QString &deviceId,
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
QMap<QString, bool> bankChecked;
|
||||
while (m_running) {
|
||||
// Устройства, которые были онлайн в БД до текущего скана
|
||||
const QStringList prevOnline = DeviceDAO::getOnlineDeviceCodes();
|
||||
|
||||
QList<QPair<QString, QString> > devices = readAdbDevices();
|
||||
QStringList onlineIds;
|
||||
QList<DeviceInfo> deviceInfos;
|
||||
@ -231,7 +248,7 @@ void DeviceScreener::start() {
|
||||
|
||||
if (status == "device") {
|
||||
onlineIds << device.id;
|
||||
const bool isChecked = bankChecked.contains(id);
|
||||
const bool isChecked = prevOnline.contains(id);
|
||||
device = readDeviceInfo(id, isChecked);
|
||||
device.image = takeScreenshot(id);
|
||||
if (!device.image.isEmpty()) {
|
||||
@ -240,9 +257,9 @@ void DeviceScreener::start() {
|
||||
}
|
||||
}
|
||||
if (!isChecked) {
|
||||
ApplicationDAO::activateAppsForDevice(id);
|
||||
// Новое устройство — включаем на сервере только active профили
|
||||
toggleBankProfilesOnApi(id, true);
|
||||
}
|
||||
bankChecked[id] = true;
|
||||
}
|
||||
device.status = status;
|
||||
device.updateTime = QDateTime::currentDateTime();
|
||||
@ -276,22 +293,14 @@ void DeviceScreener::start() {
|
||||
}
|
||||
}
|
||||
|
||||
// if (!deviceInfos.empty()) {
|
||||
// postDevicesData(deviceInfos);
|
||||
// }
|
||||
|
||||
if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) {
|
||||
qDebug() << "Cant mark devices offline";
|
||||
}
|
||||
|
||||
// Деактивировать приложения для устройств, которые ушли в офлайн
|
||||
QStringList offlineIds;
|
||||
for (auto it = bankChecked.begin(); it != bankChecked.end();) {
|
||||
if (!onlineIds.contains(it.key())) {
|
||||
ApplicationDAO::deactivateAppsForDevice(it.key());
|
||||
it = bankChecked.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
// Было онлайн в БД, сейчас офлайн — выключаем профили на сервере
|
||||
for (const QString &id : prevOnline) {
|
||||
if (!onlineIds.contains(id)) {
|
||||
toggleBankProfilesOnApi(id, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -42,4 +42,5 @@ private:
|
||||
QString createDeviceOnApi(const DeviceInfo &device);
|
||||
void updateDeviceOnApi(const DeviceInfo &device);
|
||||
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/DeviceDAO.h"
|
||||
#include "dao/EventDAO.h"
|
||||
#include "dao/TransactionDAO.h"
|
||||
#include "db/ApplicationInfo.h"
|
||||
#include "net/NetworkService.h"
|
||||
#include "birbank/GetLastDaysHistoryBirbankScript.h"
|
||||
@ -24,13 +25,14 @@
|
||||
#include "ziraat/GetLastDaysHistoryZiraatScript.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() = default;
|
||||
|
||||
void EventHandler::start() {
|
||||
qDebug() << "[EventHandler] start() called, thread:" << QThread::currentThread();
|
||||
m_network = new NetworkService(this);
|
||||
connect(m_network, &NetworkService::tasksReceived, this, &EventHandler::onTasksReceived);
|
||||
|
||||
@ -40,11 +42,17 @@ void EventHandler::start() {
|
||||
m_pollTimer->start();
|
||||
|
||||
// Первый запрос сразу при старте
|
||||
qDebug() << "[EventHandler] starting first getTasks()";
|
||||
m_network->getTasks();
|
||||
}
|
||||
|
||||
void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
||||
if (!m_running) return;
|
||||
if (!tasks.isEmpty())
|
||||
qDebug() << "[EventHandler] onTasksReceived, count:" << tasks.size();
|
||||
|
||||
// Получаем список онлайн устройств один раз
|
||||
const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes();
|
||||
|
||||
// 1. Сохраняем новые задачи в БД
|
||||
for (const auto &taskVal : tasks) {
|
||||
@ -62,9 +70,6 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Пропускаем если уже есть активная запись с таким external_id
|
||||
if (EventDAO::existsActiveByExternalId(materialId)) continue;
|
||||
|
||||
// Ищем аккаунт по material_id напрямую
|
||||
const AccountInfo account = AccountDAO::findByMaterialId(materialId);
|
||||
if (account.id == -1) {
|
||||
@ -72,7 +77,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
||||
QJsonObject data;
|
||||
data["status"] = "error";
|
||||
data["description"] = "Account not found for material_id: " + materialId;
|
||||
sendTaskResult(typeStr, materialId, bankName, data);
|
||||
sendTaskResult(typeStr, materialId, bankName, QJsonValue(data));
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -85,19 +90,48 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
||||
event.bankName = bankName;
|
||||
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;
|
||||
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. Для каждого онлайн устройства без активного потока — запускаем следующую задачу
|
||||
const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes();
|
||||
for (const QString &deviceId : onlineDevices) {
|
||||
if (const QThread *thr = m_threads.value(deviceId, nullptr); thr && thr->isRunning()) continue;
|
||||
|
||||
const EventInfo event = EventDAO::getFirstWaitEventByDeviceId(deviceId);
|
||||
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 ApplicationInfo app = ApplicationDAO::getApplication(deviceId, account.appCode);
|
||||
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,
|
||||
const QJsonObject &data) {
|
||||
const QJsonValue &data) {
|
||||
QJsonObject obj;
|
||||
obj["type"] = type;
|
||||
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)) {
|
||||
qCritical() << "[EventHandler] Failed to update event:" << event.id;
|
||||
} else {
|
||||
sendEventStatus(
|
||||
event.deviceId,
|
||||
QJsonValue taskData;
|
||||
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.id,
|
||||
eventStatusToString(newStatus),
|
||||
result
|
||||
event.bankName,
|
||||
taskData
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) {
|
||||
m_pathMaterial = settings.value("net/pathMaterial", "/api/v1/profile/material").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_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString();
|
||||
m_manager = new QNetworkAccessManager(this);
|
||||
m_token = settings.value("common/token").toString();
|
||||
m_accessToken = settings.value("common/access_token").toString();
|
||||
@ -412,6 +413,26 @@ void NetworkService::patchBankProfile() {
|
||||
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() {
|
||||
if (!ensureValidToken()) { emit finished(); return; }
|
||||
|
||||
@ -594,10 +615,21 @@ void NetworkService::getTasks() {
|
||||
if (m_fetchingTasks) return;
|
||||
m_fetchingTasks = true;
|
||||
|
||||
if (!ensureValidToken()) {
|
||||
m_fetchingTasks = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||||
const QString accessToken = settings.value("common/access_token").toString();
|
||||
|
||||
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);
|
||||
request.setRawHeader("Authorization", ("Bearer " + accessToken).toUtf8());
|
||||
|
||||
@ -607,7 +639,8 @@ void NetworkService::getTasks() {
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "[NetworkService] getTasks error:" << reply->errorString();
|
||||
qWarning() << "[NetworkService] getTasks error:" << reply->errorString()
|
||||
<< "status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -638,6 +671,37 @@ void NetworkService::postTaskResult() {
|
||||
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() {
|
||||
m_running = true;
|
||||
|
||||
|
||||
@ -28,6 +28,8 @@ public slots:
|
||||
|
||||
void patchBankProfile();
|
||||
|
||||
void toggleBankProfile();
|
||||
|
||||
void postMaterial();
|
||||
|
||||
void postDeviceScreenshot();
|
||||
@ -52,6 +54,9 @@ public slots:
|
||||
|
||||
void postTaskResult();
|
||||
|
||||
void postMonitoringLog(const QString &type, const QString &event,
|
||||
const QString &message, const QByteArray &image);
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
@ -86,6 +91,7 @@ private:
|
||||
QString m_pathMaterial;
|
||||
QString m_pathGetTasks;
|
||||
QString m_pathTaskResult;
|
||||
QString m_pathMonitoringLog;
|
||||
bool m_fetchingTasks = false;
|
||||
|
||||
QNetworkAccessManager *m_manager;
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
#include "TutorialWindow.h"
|
||||
#include "bank/BankListWindow.h"
|
||||
#include "event/EventWindow.h"
|
||||
#include "log/LogWindow.h"
|
||||
#include "db/DeviceInfo.h"
|
||||
#include "device/DeviceWidget.h"
|
||||
@ -39,10 +40,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
||||
// auto *accountPageAction = new QAction("Аккаунты", this);
|
||||
auto *bankListAction = new QAction("Банки", this);
|
||||
auto *tutorialPageAction = new QAction("Инструкция", this);
|
||||
auto *eventAction = new QAction("События", this);
|
||||
auto *logAction = new QAction("Логи", this);
|
||||
menu->addAction(devicesPageAction);
|
||||
// menu->addAction(accountPageAction);
|
||||
menu->addAction(bankListAction);
|
||||
menu->addAction(eventAction);
|
||||
menu->addAction(tutorialPageAction);
|
||||
menu->addAction(logAction);
|
||||
|
||||
@ -76,6 +79,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
||||
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, [=]() {
|
||||
if (!m_logWindow) {
|
||||
m_logWindow = new LogWindow(this);
|
||||
|
||||
@ -28,6 +28,7 @@ private:
|
||||
QWidget *m_tutorialWindow = nullptr;
|
||||
QWidget *m_bankListWindow = nullptr;
|
||||
QWidget *m_logWindow = nullptr;
|
||||
QWidget *m_eventWindow = nullptr;
|
||||
|
||||
void createDevicePage();
|
||||
|
||||
|
||||
@ -359,8 +359,23 @@ void AccountSettingsWindow::startGetLastTransactions(const QString &appCode) {
|
||||
}
|
||||
|
||||
void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) {
|
||||
Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status)
|
||||
// TODO: заменить на новый API
|
||||
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code);
|
||||
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) {
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
#include "dao/ApplicationDAO.h"
|
||||
#include "db/ApplicationInfo.h"
|
||||
#include "net/NetworkService.h"
|
||||
#include "common/PaddedItemDelegate.h"
|
||||
#include "device/EditBankDialog.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) {
|
||||
Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status)
|
||||
// TODO: заменить на новый API
|
||||
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code);
|
||||
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) {
|
||||
|
||||
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 <QHeaderView>
|
||||
#include <QPixmap>
|
||||
#include <QResizeEvent>
|
||||
#include <QScrollArea>
|
||||
#include <QSplitter>
|
||||
#include <QVBoxLayout>
|
||||
@ -127,16 +128,27 @@ void LogWindow::updatePagination() {
|
||||
void LogWindow::showScreenshot(int logId) {
|
||||
const QByteArray bytes = AppLogDAO::getScreenshot(logId);
|
||||
if (bytes.isEmpty()) {
|
||||
m_originalPixmap = {};
|
||||
m_screenshotLabel->setPixmap({});
|
||||
m_screenshotLabel->setText("Скриншот отсутствует");
|
||||
return;
|
||||
}
|
||||
QPixmap px;
|
||||
if (!px.loadFromData(bytes)) {
|
||||
if (!m_originalPixmap.loadFromData(bytes)) {
|
||||
m_originalPixmap = {};
|
||||
m_screenshotLabel->setText("Не удалось загрузить скриншот");
|
||||
return;
|
||||
}
|
||||
rescaleScreenshot();
|
||||
}
|
||||
|
||||
void LogWindow::rescaleScreenshot() {
|
||||
if (m_originalPixmap.isNull()) return;
|
||||
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_totalPages = 1;
|
||||
|
||||
QPixmap m_originalPixmap;
|
||||
|
||||
void loadPage();
|
||||
void updatePagination();
|
||||
void showScreenshot(int logId);
|
||||
void rescaleScreenshot();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user