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.
347 lines
15 KiB
C++
347 lines
15 KiB
C++
#include "EventHandler.h"
|
|
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QMap>
|
|
#include <QThread>
|
|
#include <QTimer>
|
|
|
|
#include "dao/AccountDAO.h"
|
|
#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"
|
|
#include "birbank/LoginAndCheckAccountsBirbankScript.h"
|
|
#include "ozon/GetLastTransactionsScript.h"
|
|
#include "ozon/LoginAndCheckAccountsScript.h"
|
|
#include "rshb/GetLastDaysHistoryScript.h"
|
|
#include "rshb/LoginAndCheckAccountsScript.h"
|
|
#include "toss/GetLastDaysHistoryTossScript.h"
|
|
#include "toss/LoginAndCheckAccountsTossScript.h"
|
|
#include "ziraat/GetLastDaysHistoryZiraatScript.h"
|
|
#include "ziraat/LoginAndCheckAccountsZiraatScript.h"
|
|
|
|
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);
|
|
|
|
m_pollTimer = new QTimer(this);
|
|
m_pollTimer->setInterval(5000);
|
|
connect(m_pollTimer, &QTimer::timeout, m_network, &NetworkService::getTasks);
|
|
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) {
|
|
const QJsonObject task = taskVal.toObject();
|
|
const QString bankName = task["bank_name"].toString();
|
|
const QString materialId = task["material_id"].isString()
|
|
? task["material_id"].toString()
|
|
: QString::number(task["material_id"].toInt());
|
|
const QString typeStr = task["type"].toString();
|
|
|
|
const EventType eventType = eventTypeFromString(typeStr);
|
|
|
|
if (eventType == EventType::Unknown) {
|
|
qWarning() << "[EventHandler] Unknown task type:" << typeStr;
|
|
continue;
|
|
}
|
|
|
|
// Ищем аккаунт по material_id напрямую
|
|
const AccountInfo account = AccountDAO::findByMaterialId(materialId);
|
|
if (account.id == -1) {
|
|
qWarning() << "[EventHandler] Account not found for bank:" << bankName << "material_id:" << materialId;
|
|
QJsonObject data;
|
|
data["status"] = "error";
|
|
data["description"] = "Account not found for material_id: " + materialId;
|
|
sendTaskResult(typeStr, materialId, bankName, QJsonValue(data));
|
|
continue;
|
|
}
|
|
|
|
EventInfo event;
|
|
event.status = EventStatus::Wait;
|
|
event.type = eventType;
|
|
event.externalId = materialId;
|
|
event.accountId = account.id;
|
|
event.deviceId = account.deviceId;
|
|
event.bankName = bankName;
|
|
event.timestamp = QDateTime::currentDateTimeUtc();
|
|
|
|
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. Для каждого онлайн устройства без активного потока — запускаем следующую задачу
|
|
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;
|
|
|
|
if (!EventDAO::updateEvent(event.id, EventStatus::InProgress, "")) {
|
|
qCritical() << "[EventHandler] Failed to set InProgress, event id:" << event.id;
|
|
continue;
|
|
}
|
|
|
|
startThreadForTask(account, event);
|
|
}
|
|
}
|
|
|
|
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName,
|
|
const QJsonValue &data) {
|
|
QJsonObject obj;
|
|
obj["type"] = type;
|
|
obj["material_id"] = materialId; // UUID string
|
|
obj["bank_name"] = bankName;
|
|
obj["data"] = data;
|
|
|
|
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 void sendEventStatus(const QString &deviceId, const QString &externalId, const int eventId,
|
|
const QString &status, const QString &error) {
|
|
QJsonObject obj;
|
|
obj["id"] = externalId;
|
|
obj["externalId"] = eventId;
|
|
obj["status"] = status;
|
|
obj["error"] = error;
|
|
|
|
auto *paymentThread = new QThread;
|
|
auto *networkService = new NetworkService;
|
|
networkService->moveToThread(paymentThread);
|
|
networkService->setDeviceId(deviceId);
|
|
networkService->setPayload(QJsonDocument(obj).toJson());
|
|
QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::postEventStatus);
|
|
QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit);
|
|
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
|
|
QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater);
|
|
paymentThread->start();
|
|
}
|
|
|
|
void EventHandler::updateEventThread(const QString &key, const EventInfo &event, const QString &result) {
|
|
const EventStatus newStatus = result.isEmpty() ? EventStatus::Complete : EventStatus::Error;
|
|
|
|
if (!EventDAO::updateEvent(event.id, newStatus, result)) {
|
|
qCritical() << "[EventHandler] Failed to update event:" << event.id;
|
|
} else {
|
|
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.bankName,
|
|
taskData
|
|
);
|
|
}
|
|
|
|
if (const auto tm = m_timers.take(key)) {
|
|
tm->stop();
|
|
tm->deleteLater();
|
|
}
|
|
m_threads.remove(key);
|
|
}
|
|
|
|
void EventHandler::startThreadForTask(const AccountInfo &account, const EventInfo &event) {
|
|
const auto thr = new QThread(this);
|
|
const QString key = account.deviceId;
|
|
|
|
// Generic helper: подключает сигналы и переносит worker в поток
|
|
auto setup = [&](auto *worker) {
|
|
using W = std::remove_pointer_t<decltype(worker)>;
|
|
worker->moveToThread(thr);
|
|
connect(thr, &QThread::started, worker, &W::start);
|
|
connect(worker, &W::finished, thr, &QThread::quit);
|
|
connect(worker, &W::finished, worker, &QObject::deleteLater);
|
|
connect(thr, &QThread::finished, thr, &QObject::deleteLater);
|
|
connect(worker, &W::finishedWithResult, this,
|
|
[this, key, event](const QString &result) {
|
|
updateEventThread(key, event, result);
|
|
}
|
|
);
|
|
};
|
|
|
|
const QString &appCode = account.appCode;
|
|
const QString &deviceId = account.deviceId;
|
|
|
|
if (event.type == EventType::FetchProfile) {
|
|
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(deviceId, appCode));
|
|
else if (appCode == "birbank") setup(new LoginAndCheckAccountsBirbankScript(deviceId, appCode));
|
|
else if (appCode == "rshb") setup(new Rshb::LoginAndCheckAccountsScript(deviceId, appCode));
|
|
else if (appCode == "toss") setup(new LoginAndCheckAccountsTossScript(deviceId, appCode));
|
|
else if (appCode == "ziraat") setup(new LoginAndCheckAccountsZiraatScript(deviceId, appCode));
|
|
else qWarning() << "[EventHandler] FETCH_PROFILE: unknown appCode:" << appCode;
|
|
}
|
|
|
|
if (event.type == EventType::FetchTransactions) {
|
|
if (appCode == "birbank") setup(new GetLastDaysHistoryBirbankScript(account));
|
|
else if (appCode == "ozon") setup(new Ozon::GetLastTransactionsScript(deviceId, appCode));
|
|
else if (appCode == "rshb") setup(new Rshb::GetLastDaysHistoryScript(account));
|
|
else if (appCode == "toss") setup(new GetLastDaysHistoryTossScript(account));
|
|
else if (appCode == "ziraat") setup(new GetLastDaysHistoryZiraatScript(account));
|
|
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
|
}
|
|
|
|
if (event.type == EventType::CreateTransaction) {
|
|
// TODO: реализовать CREATE_TRANSACTION
|
|
}
|
|
|
|
// Watchdog-таймер: 5 минут — убиваем зависший поток
|
|
const auto timer = new QTimer(this);
|
|
timer->setSingleShot(true);
|
|
connect(timer, &QTimer::timeout, this, [this, key]() {
|
|
qWarning() << "[EventHandler] Timeout for key" << key << "- killing stuck thread.";
|
|
if (QThread *thread = m_threads.value(key); thread && thread->isRunning()) {
|
|
thread->requestInterruption();
|
|
if (!thread->wait(500)) {
|
|
qWarning() << "[EventHandler] Hard terminate for key" << key;
|
|
thread->terminate();
|
|
thread->wait();
|
|
}
|
|
}
|
|
if (const auto tm = m_timers.take(key)) {
|
|
tm->deleteLater();
|
|
}
|
|
m_threads.remove(key);
|
|
});
|
|
|
|
m_threads[key] = thr;
|
|
m_timers[key] = timer;
|
|
|
|
timer->start(5 * 60 * 1000);
|
|
thr->start();
|
|
}
|
|
|
|
void EventHandler::clearAll() {
|
|
for (QThread *thread : m_threads) {
|
|
if (thread->isRunning()) {
|
|
thread->requestInterruption();
|
|
thread->terminate();
|
|
thread->wait();
|
|
}
|
|
thread->deleteLater();
|
|
}
|
|
m_threads.clear();
|
|
|
|
for (QTimer *timer : m_timers) {
|
|
timer->stop();
|
|
timer->deleteLater();
|
|
}
|
|
m_timers.clear();
|
|
}
|
|
|
|
void EventHandler::stop() {
|
|
m_running = false;
|
|
if (m_pollTimer) m_pollTimer->stop();
|
|
clearAll();
|
|
}
|