1
0
forked from BRT/arc

Refactor and enhance device and event handling logic

Refactored `DeviceScreener`, `NetworkService`, and event-related logic for improved maintainability and flexibility. Introduced `EventHandler` for thread management and made changes to include configurable URLs via `config.ini`. Modified database schema to support device-level event linking.
This commit is contained in:
slava 2025-05-22 21:33:25 +07:00
parent 89ea2ac53a
commit 5cb32ec4f1
16 changed files with 322 additions and 54 deletions

View File

@ -6,6 +6,8 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
add_compile_definitions(QT_MESSAGELOGCONTEXT)
if (WIN32)
# Укажи путь к Qt6
set(CMAKE_PREFIX_PATH "C:/Qt/6.9.0/msvc2022_arm64")
@ -75,6 +77,7 @@ if (WIN32)
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/database/app_data.db" DESTINATION "${CMAKE_BINARY_DIR}")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/images" DESTINATION "${CMAKE_BINARY_DIR}")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/rus.traineddata" DESTINATION "${CMAKE_BINARY_DIR}")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/config.ini" DESTINATION "${CMAKE_BINARY_DIR}")
elseif (APPLE)
target_link_libraries(ARCDesktopProject

View File

@ -1,6 +1,16 @@
[common]
varsion=0.0.1
[apps]
list=rshb
[rshb]
name=Рос Сельхоз Банк
name=Росcельхозбанк
android_package_name=ru.rshb.dbo
[net]
appInfoUpdate="http://localhost:9999/api/v1/app/update"
accountUpdate=http://localhost:9999/api/v1/account/update
paymentsGet=http://localhost:9999/api/v1/payments
transactionUpdate=http://localhost:9999/api/v1/transaction/update
transactionInsert=http://localhost:9999/api/v1/transaction/insert

Binary file not shown.

View File

@ -45,6 +45,28 @@ QList<DeviceInfo> DeviceDAO::getAllDevices(const bool online) {
return devices;
}
QList<QString> DeviceDAO::getOnlineDeviceCodes() {
QList<QString> codes;
QSqlQuery query(DatabaseManager::instance().database());
QString sql = R"(
SELECT id
FROM devices
WHERE image IS NOT NULL AND status = 'device'
)";
query.prepare(sql);
if (!query.exec()) {
qWarning() << "Ошибка при получении списка устройств:" << query.lastError().text();
return {};
}
while (query.next()) {
codes.append(query.value("id").toString());
}
return codes;
}
DeviceInfo DeviceDAO::getDeviceById(const QString &deviceId) {
QSqlQuery query(DatabaseManager::instance().database());

View File

@ -13,4 +13,6 @@ public:
[[nodiscard]] static bool updateImage(const QString &deviceId, const QByteArray &imageData);
[[nodiscard]] static QList<DeviceInfo> getAllDevices(bool online);
[[nodiscard]] static QList<QString> getOnlineDeviceCodes();
};

View File

@ -12,6 +12,7 @@ EventInfo parseEvent(const QSqlQuery &query) {
event.status = eventStatusFromString(query.value("status").toString());
event.externalId = query.value("external_id").toInt();
event.accountId = query.value("account_id").toInt();
event.deviceId = query.value("device_id").toString();
event.amount = query.value("amount").toDouble();
event.phone = query.value("phone").toString();
event.bankName = query.value("bank_name").toString();
@ -26,22 +27,23 @@ int EventDAO::insertEvent(const EventInfo &event) {
query.prepare(R"(
INSERT INTO events (
status, external_id, account_id, amount, phone, bank_name, timestamp
status, external_id, account_id, amount, phone, bank_name, timestamp, device_id
) VALUES (
:status, :external_id, :account_id, :amount, :phone, :bank_name, :timestamp
:status, :external_id, :account_id, :amount, :phone, :bank_name, :timestamp, :device_id
)
)");
query.bindValue(":status", eventStatusToString(event.status));
query.bindValue(":external_id", event.externalId);
query.bindValue(":account_id", event.accountId);
query.bindValue(":device_id", event.deviceId);
query.bindValue(":amount", event.amount);
query.bindValue(":phone", event.phone);
query.bindValue(":bank_name", event.bankName);
query.bindValue(":timestamp", DateUtils::toUtcString(DateUtils::getUtcNow()));
if (!query.exec()) {
qWarning() << "Ошибка при вставке event:" << query.lastError().text();
qCritical() << "Ошибка при вставке event:" << query.lastError().text();
return -1;
}
@ -78,7 +80,7 @@ bool EventDAO::updateEvent(
}
if (!query.exec()) {
qWarning() << "Ошибка при обновлении event:" << query.lastError().text();
qCritical() << "Ошибка при обновлении event:" << query.lastError().text();
return false;
}
return true;
@ -89,14 +91,14 @@ QList<EventInfo> EventDAO::getAllEvents(const EventStatus &status) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, account_id, external_id, amount, phone, bank_name, status, update_time, complete_time, timestamp
SELECT id, account_id, device_id, external_id, amount, phone, bank_name, status, update_time, complete_time, timestamp
FROM events
WHERE status = :status
)");
query.bindValue(":status", eventStatusToString(status));
if (!query.exec()) {
qWarning() << "Ошибка при получении списка устройств:" << query.lastError().text();
qWarning() << "Ошибка при получении getAllEvents:" << query.lastError().text();
return events;
}
@ -106,3 +108,26 @@ QList<EventInfo> EventDAO::getAllEvents(const EventStatus &status) {
return events;
}
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
FROM events
WHERE status = :status AND device_id = :device_id
ORDER BY timestamp ASC
LIMIT 1
)");
query.bindValue(":status", eventStatusToString(EventStatus::Wait));
query.bindValue(":device_id", key);
if (!query.exec()) {
qCritical() << "Ошибка при получении getFirstWaitEventByDeviceId:" << query.lastError().text();
return {};
}
while (query.next()) {
return parseEvent(query);
}
return {};
}

View File

@ -12,4 +12,6 @@ public:
);
[[nodiscard]] static QList<EventInfo> getAllEvents(const EventStatus &status);
[[nodiscard]] static EventInfo getFirstWaitEventByDeviceId(const QString &key);
};

View File

@ -16,6 +16,7 @@
#include <chrono>
#include <QFile>
#include "EventHandler.h"
#include "net/NetworkService.h"
#include "rshb/GetLastDaysHistoryScript.h"
#include "rshb/HomeScreenScript.h"
@ -145,7 +146,7 @@ static QFile logFile;
static QMutex logMutex;
void messageHandler(
QtMsgType type,
const QtMsgType type,
const QMessageLogContext &context,
const QString &msg
) {
@ -166,8 +167,9 @@ void messageHandler(
break;
}
const QString line = QString("%1 [%2] %3\n").arg(timestamp, level, msg);
const QString line = QString("%1 [%2] [%3:%4] %5\n")
.arg(timestamp, level, context.function, QString::number(context.line), msg);
// Записываем в файл (потокобезопасно)
{
QMutexLocker locker(&logMutex);
@ -178,7 +180,7 @@ void messageHandler(
}
// По желанию — дублировать в консоль
QByteArray localMsg = line.toLocal8Bit();
const QByteArray localMsg = line.toLocal8Bit();
fprintf(stderr, "%s", localMsg.constData());
if (type == QtFatalMsg) {
@ -196,21 +198,28 @@ int main(int argc, char *argv[]) {
// Устанавливаем свой обработчик
qInstallMessageHandler(messageHandler);
auto *thread = new QThread;
auto *worker = new NetworkService;
// FIXME доставить десктоп ид из настроеек
worker->setDeviceData("DT88bokcwQ", "");
auto *worker = new EventHandler;
worker->moveToThread(thread);
QObject::connect(thread, &QThread::started, worker, &NetworkService::getPayments);
QObject::connect(worker, &NetworkService::finished, thread, &QThread::quit);
QObject::connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
QObject::connect(thread, &QThread::started, worker, &EventHandler::start);
QObject::connect(worker, &EventHandler::finished, thread, &QThread::quit);
QObject::connect(worker, &EventHandler::finished, worker, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
// setupScriptAutoPaymentAndThread(app);
// setupWorkerAndThread(app);
auto *paymentThread = new QThread;
auto *networkService = new NetworkService;
// FIXME доставить десктоп ид из настроеек
networkService->setDeviceData("DT88bokcwQ", "");
networkService->moveToThread(paymentThread);
QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::start);
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();
setupWorkerAndThread(app);
MainWindow window;
window.show();

View File

@ -74,6 +74,7 @@ CREATE TABLE IF NOT EXISTS events
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
external_id INTEGER,
device_id TEXT,
amount REAL,
phone TEXT,
@ -85,5 +86,6 @@ CREATE TABLE IF NOT EXISTS events
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,
UNIQUE (external_id)
);

View File

@ -31,6 +31,7 @@ struct EventInfo {
int id = -1;
int externalId = -1;
int accountId = -1;
QString deviceId;
double amount = 0;
QString phone;

View File

@ -1,6 +1,6 @@
#include "DeviceScreener.h"
#include <QRegularExpression>
#include <QSettings>
#include <QProcess>
#include <QFile>
#include <QBuffer>
@ -16,11 +16,13 @@
#include <db/DeviceInfo.h>
DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) {
const QSettings settings("config.ini", QSettings::IniFormat);
m_urlAppUpdate = settings.value("net/appInfoUpdate").toString();
}
DeviceScreener::~DeviceScreener() = default;
void postDevicesData(const QList<DeviceInfo> &devices) {
void DeviceScreener::postDevicesData(const QList<DeviceInfo> &devices) {
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart jsonPart;
@ -60,7 +62,7 @@ void postDevicesData(const QList<DeviceInfo> &devices) {
multiPart->append(uniquePart);
QNetworkAccessManager manager; // создаём внутри потока
QUrl url("http://localhost:9999/api/v1/app/update");
QUrl url(m_urlAppUpdate);
QNetworkRequest request(url);
// делаем запрос
@ -82,10 +84,12 @@ void postDevicesData(const QList<DeviceInfo> &devices) {
QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject root = doc.object();
qDebug() << "Успешный ответ: " << root.value("success").toBool();
if (!root.value("success").toBool()) {
qWarning() << root;
}
}
} else {
qWarning() << "Ошибка:" << reply->errorString();
qWarning() << "Response error:" << reply->errorString() << m_urlAppUpdate;
}
reply->deleteLater();
}
@ -116,12 +120,14 @@ void DeviceScreener::start() {
}
}
postDevicesData(deviceInfos);
if (!deviceInfos.empty()) {
postDevicesData(deviceInfos);
}
if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) {
qDebug() << "Cant mark devices offline";
}
QThread::sleep(3);
QThread::msleep(1000);
}
emit finished();
}

View File

@ -21,10 +21,13 @@ signals:
private:
bool m_running = true;
QString m_urlAppUpdate;
static QList<QPair<QString, QString> > readAdbDevices();
static QByteArray takeScreenshot(const QString &deviceId);
static DeviceInfo readDeviceInfo(const QString &deviceId);
void postDevicesData(const QList<DeviceInfo> &devices);
};

130
services/EventHandler.cpp Normal file
View File

@ -0,0 +1,130 @@
#include "EventHandler.h"
#include <QMap>
#include <QThread>
#include <QTimer>
#include "dao/AccountDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/EventDAO.h"
#include "rshb/CommonScript.h"
#include "rshb/PayByPhoneScript.h"
EventHandler::EventHandler(QObject *parent) : QObject(parent) {
m_checkTimer = new QTimer(this);
m_checkTimer->setInterval(1000);
connect(m_checkTimer, &QTimer::timeout, this, &EventHandler::checkAndRestart);
}
EventHandler::~EventHandler() = default;
void EventHandler::start() {
m_devices = DeviceDAO::getOnlineDeviceCodes();
clearAll();
// Запустим сразу потоки для всех ключей
for (auto &key: m_devices) {
startThreadForKey(key);
}
m_checkTimer->start();
}
void EventHandler::checkAndRestart() {
if (!m_running) {
return;
}
m_devices = DeviceDAO::getOnlineDeviceCodes();
for (const auto &key: m_devices) {
const QThread *thr = m_threads.value(key, nullptr);
if (!thr || !thr->isRunning()) {
startThreadForKey(key);
}
}
}
void EventHandler::startThreadForKey(const QString &key) {
const EventInfo event = EventDAO::getFirstWaitEventByDeviceId(key);
if (event.id == -1) {
return;
}
if (!EventDAO::updateEvent(event.id, EventStatus::InProgress)) {
qCritical() << "Cant update event: " << event.id;
return;
}
qDebug() << "Thread for key" << key << "running";
// FIXME пока это только оплата,
const AccountInfo account = AccountDAO::getAccountById(event.accountId);
const auto thr = new QThread(this);
auto *worker = new PayByPhoneScript(account, event.phone, event.bankName, event.amount);
worker->moveToThread(thr);
connect(thr, &QThread::started, worker, &PayByPhoneScript::start);
connect(worker, &PayByPhoneScript::finished, thr, &QThread::quit);
connect(worker, &PayByPhoneScript::finished, worker, &QObject::deleteLater);
connect(thr, &QThread::finished, thr, &QObject::deleteLater);
// При нормальном завершении — перезапустить новый поток
connect(worker, &PayByPhoneScript::finished, this,
[this, key]() {
// qDebug() << "Thread finished for key" << key << "- restarting.";
if (const auto tm = m_timers.take(key)) {
tm->stop();
tm->deleteLater();
}
m_threads.remove(key);
});
// Таймер на 5 минут — если не успел finish(), убить и перезапустить
const auto timer = new QTimer(this);
timer->setSingleShot(true);
connect(timer, &QTimer::timeout, this, [this, key]() {
qWarning() << "Timeout for key" << key << "- killing stuck thread.";
if (QThread *thread = m_threads.value(key); thread && thread->isRunning()) {
thread->requestInterruption(); // мягкое прерывание
if (!thread->wait(500)) {
qWarning() << "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;
m_checkTimer->stop();
clearAll();
}

36
services/EventHandler.h Normal file
View File

@ -0,0 +1,36 @@
#pragma once
#include <QMap>
#include <QObject>
#include <QTimer>
class EventHandler final : public QObject {
Q_OBJECT
public:
explicit EventHandler(QObject *parent = nullptr);
~EventHandler() override;
public slots:
void start();
void stop();
signals:
void finished();
private slots:
void checkAndRestart(); // вызывается по таймеру
private:
bool m_running = true;
QMap<QString, QThread *> m_threads;
QMap<QString, QTimer *> m_timers;
QTimer *m_checkTimer = nullptr;
QStringList m_devices;
void clearAll();
void startThreadForKey(const QString &key);
};

View File

@ -1,11 +1,13 @@
#include "NetworkService.h"
#include <QEventLoop>
#include <QUrlQuery>
#include <QHttpPart>
#include <QNetworkReply>
#include <QJsonObject>
#include <QJsonArray>
#include <QThread>
#include <QSettings>
#include <QTimer>
#include "dao/AccountDAO.h"
#include "dao/EventDAO.h"
@ -13,6 +15,12 @@
#include "db/EventInfo.h"
NetworkService::NetworkService(QObject *parent) : QObject(parent) {
const QSettings settings("config.ini", QSettings::IniFormat);
m_urlAccountUpdate = settings.value("net/accountUpdate").toString();
m_urlPaymentsGet = settings.value("net/paymentsGet").toString();
m_urlTransactionUpdate = settings.value("net/transactionUpdate").toString();
m_urlTransactionInsert = settings.value("net/transactionInsert").toString();
m_manager = new QNetworkAccessManager(this);
}
NetworkService::~NetworkService() = default;
@ -40,11 +48,10 @@ void addFormMetadata(QHttpMultiPart *multiPart, const QByteArray &fieldValue) {
}
QJsonValue NetworkService::post(QHttpMultiPart *multiPart, const QString &path) {
QNetworkAccessManager manager; // создаём внутри потока
const QUrl url(path);
const QNetworkRequest request(url);
QNetworkReply *reply = manager.post(request, multiPart);
QNetworkReply *reply = m_manager->post(request, multiPart);
multiPart->setParent(reply);
QEventLoop loop;
@ -64,20 +71,17 @@ QJsonValue NetworkService::post(QHttpMultiPart *multiPart, const QString &path)
if (root.value("success").toBool()) {
result = root.value("data");
} else {
qWarning() << "Ошибка:" << root;
qCritical() << "Ошибка:" << root;
}
}
} else {
qWarning() << "Ошибка:" << reply->errorString();
qCritical() << "Ошибка:" << reply->errorString() << path;
}
reply->deleteLater();
return result;
}
QJsonArray NetworkService::get(const QString &path, const QMap<QString, QString> &params) {
QNetworkAccessManager manager; // создаём внутри потока
QUrl url(path);
QUrlQuery query;
for (auto it = params.constBegin(); it != params.constEnd(); ++it) {
@ -86,7 +90,8 @@ QJsonArray NetworkService::get(const QString &path, const QMap<QString, QString>
url.setQuery(query);
const QNetworkRequest request(url);
QNetworkReply *reply = manager.get(request);
QNetworkReply *reply = m_manager->get(request);
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> guard(reply);
QEventLoop loop;
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
@ -121,9 +126,9 @@ void NetworkService::postAccountsData() {
addFormMetadata(multiPart, m_json);
addFormField(multiPart, "desktopUniqueName", m_desktopId);
addFormField(multiPart, "androidUniqueName", m_deviceId);
const bool isSuccess = post(multiPart, "http://localhost:9999/api/v1/account/update").toBool();
const bool isSuccess = post(multiPart, m_urlAccountUpdate).toBool();
if (!isSuccess) {
qDebug() << "Cant post accounts data";
qCritical() << "Cant post accounts data";
}
emit finished();
@ -132,7 +137,7 @@ void NetworkService::postAccountsData() {
void NetworkService::getPayments() {
QMap<QString, QString> params;
params["desktopUniqueName"] = m_desktopId;
const QJsonArray data = get("http://localhost:9999/api/v1/payments", params);
const QJsonArray data = get(m_urlPaymentsGet, params);
for (QJsonValue item: data) {
QJsonObject obj = item.toObject();
@ -149,20 +154,16 @@ void NetworkService::getPayments() {
AccountInfo account = AccountDAO::findAppAccount(deviceId, appName, lastNumbers);
eventInfo.accountId = account.id;
eventInfo.deviceId = account.deviceId;
int id = EventDAO::insertEvent(eventInfo);
if (id == -1) {
qDebug() << "Cant insert event";
} else {
if (!EventDAO::updateEvent(id, EventStatus::Wait)) {
qDebug() << "Cant update event";
}
if (int id = EventDAO::insertEvent(eventInfo); id == -1) {
qCritical() << "Cant insert event: " << eventInfo.externalId;
}
}
emit finished();
if (!m_running) {
emit finished();
}
}
@ -174,9 +175,9 @@ void NetworkService::updateTransactionData() {
addFormField(multiPart, "androidUniqueName", m_deviceId);
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
const bool isSuccess = post(multiPart, "http://localhost:9999/api/v1/transaction/update").toBool();
const bool isSuccess = post(multiPart, m_urlTransactionUpdate).toBool();
if (!isSuccess) {
qDebug() << "Cant update transaction";
qCritical() << "Cant update transaction: updateTransactionData";
}
emit finished();
@ -190,16 +191,23 @@ void NetworkService::insertTransactionData() {
addFormField(multiPart, "androidUniqueName", m_deviceId);
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
const int id = post(multiPart, "http://localhost:9999/api/v1/transaction/insert").toInt();
const int id = post(multiPart, m_urlTransactionInsert).toInt();
if (id > 0) {
const int transactionId = m_extraData["transactionId"].toInt();
if (!TransactionDAO::updateExternalTransactionId(transactionId, id)) {
qDebug() << "Cant update transaction id";
qCritical() << "Cant update transaction id: " << id;
}
}
emit finished();
}
void NetworkService::start() {
QTimer *poller = new QTimer(this);
poller->setInterval(1000);
connect(poller, &QTimer::timeout, this, &NetworkService::getPayments);
poller->start();
}
void NetworkService::stop() {
m_running = false;
}

View File

@ -1,5 +1,6 @@
#pragma once
#include <qhttpmultipart.h>
#include <QNetworkAccessManager>
#include <QObject>
#include <QMap>
@ -29,6 +30,7 @@ public slots:
void updateTransactionData();
void start();
void stop();
signals:
@ -41,6 +43,13 @@ private:
QMap<QString, QVariant> m_extraData = {};
bool m_running = true;
QString m_urlAccountUpdate;
QString m_urlPaymentsGet;
QString m_urlTransactionUpdate;
QString m_urlTransactionInsert;
QNetworkAccessManager *m_manager;
QJsonValue post(QHttpMultiPart *multiPart, const QString &path);