#include "EventHandler.h" #include #include #include #include #include #include #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(account.amount); obj["currency"] = 643; // RUB taskData = obj; } else if (event.type == EventType::FetchTransactions) { QList 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(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; 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(); }