1
0
forked from BRT/arc

Refactor device and account data handling, enhance UI.

Introduced a new `NetworkService` for posting account and device data efficiently. Improved the transaction UI with styled tables and added lazy content loading. Changed screenshot handling to return JPEG data directly, and restructured device processing for better error handling and data integration.
This commit is contained in:
slava 2025-05-18 23:42:23 +07:00
parent c4f171259b
commit edcd931e66
12 changed files with 408 additions and 74 deletions

View File

@ -22,6 +22,7 @@ elseif (APPLE)
endif ()
find_package(Qt6 REQUIRED COMPONENTS
Network
Widgets
Sql
Xml
@ -66,6 +67,7 @@ if (WIN32)
Qt6::Widgets
Qt6::Xml
Qt6::Sql
Qt6::Network
Tesseract::libtesseract
)
@ -79,6 +81,7 @@ elseif (APPLE)
Qt6::Widgets
Qt6::Sql
Qt6::Xml
Qt6::Network
tesseract
)
endif ()

View File

@ -1,5 +1,7 @@
#include "GetLastDaysHistoryScript.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QRandomGenerator>
#include <QThread>
#include <utility>
@ -10,6 +12,7 @@
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "db/DeviceInfo.h"
#include "net/NetworkService.h"
#include "time/DateUtils.h"
GetLastDaysHistoryScript::GetLastDaysHistoryScript(
@ -134,6 +137,35 @@ bool GetLastDaysHistoryScript::openAndSaveDitailInfo(TransactionInfo &transactio
return false;
}
void postAccountsData(const QList<QPair<AccountInfo, Node> > &accounts) {
if (accounts.isEmpty()) {
return;
}
auto *thread = new QThread;
auto *worker = new NetworkService;
QJsonArray list;
for (const QPair<AccountInfo, Node> &pair: accounts) {
QJsonObject obj;
AccountInfo account = pair.first;
obj["appName"] = account.appName;
obj["lastNumbers"] = account.lastNumbers;
obj["amount"] = account.amount;
obj["description"] = account.description;
list.append(obj);
}
worker->setDeviceData("DT88bokcwQ", accounts.first().first.deviceId);
worker->setPayload(QJsonDocument(list).toJson());
worker->moveToThread(thread);
QObject::connect(thread, &QThread::started, worker, &NetworkService::postAccountsData);
QObject::connect(worker, &NetworkService::finished, thread, &QThread::quit);
QObject::connect(worker, &NetworkService::finished, worker, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
void GetLastDaysHistoryScript::doStart() {
// берем все транзакции из БД
QList<TransactionInfo> localTransactions = TransactionDAO::getTransactionsWithinHours(m_account.id, 48);
@ -141,8 +173,17 @@ void GetLastDaysHistoryScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter));
if (xmlScreenParser.isHomeScreen()) {
// парсим все транзакции
if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) {
QList<QPair<AccountInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
for (auto &[account, node]: accounts) {
account.deviceId = m_account.deviceId;
account.appName = m_account.appName;
if (!AccountDAO::upsertAccount(account)) {
qDebug() << "Not updated: " << account.lastNumbers;
}
}
postAccountsData(accounts);
findAndTapOnAccount(device.id, m_account.lastNumbers);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter));
@ -339,7 +380,6 @@ void GetLastDaysHistoryScript::doStart() {
// Если по карте, мы знаем ток дату + сумма
}
}
} else {
qDebug() << "--- home screen not found";

Binary file not shown.

View File

@ -75,7 +75,7 @@ void setupScriptAutoPaymentAndThread(QCoreApplication &app) {
const QString phoneNumber = "79641815146";
const QString bankName = "Альфа-Банк";
// const QString bankName = "Сбербанк";
const double amount = 440;
const double amount = 520;
const AccountInfo account = AccountDAO::findAppAccount(appName, cardNumber);
if (account.id != -1) {
@ -136,31 +136,10 @@ void setupScriptAutoPaymentAndThread(QCoreApplication &app) {
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// QList<Card> cards = AccountInfoScreener::parseCardData(QByteArray());
// for (const Card &card: cards) {
// AccountInfo info;
// info.deviceId = "6edc4a47";
// info.appName = "bank";
// info.cardName = card.name;
// info.amount = card.amount;
// info.description = card.description;
// info.status = "active";
// info.updateTime = QDateTime::currentDateTime();
// qDebug() << "Account info: " << AccountDAO::upsertAccount(info);
// }
setupWorkerAndThreadHistory(app);
setupWorkerAndThread(app);
setupScriptAutoPaymentAndThread(app);
// setupWorkerAndThread(app);
//
// const QDateTime current = QDateTime::currentDateTimeUtc();
// qDebug() << "Обновление lastScan:" << CommonDataDAO::updateLastScan(current);
//
// const QDateTime savedTime = CommonDataDAO::getLastScan();
// qDebug() << "Сохранившееся время:" << savedTime.toString();
//
//
// MainWindow window;
// window.show();
return app.exec();
MainWindow window;
window.show();
return QApplication::exec();
}

View File

@ -4,8 +4,14 @@
#include <QProcess>
#include <QFile>
#include <QBuffer>
#include <qeventloop.h>
#include <QThread>
#include <QImage>
#include <QHttpMultiPart>
#include <QJsonObject>
#include <QJsonArray>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <dao/DeviceDAO.h>
#include <db/DeviceInfo.h>
@ -14,27 +20,107 @@ DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) {
DeviceScreener::~DeviceScreener() = default;
void postDevicesData(const QList<DeviceInfo> &devices) {
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart jsonPart;
jsonPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/json; charset=UTF-8");
jsonPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="metadata")");
QJsonArray list;
for (const DeviceInfo &device: devices) {
QJsonObject obj;
obj["uniqueName"] = device.id;
obj["status"] = device.status;
obj["name"] = device.name;
obj["screenWidth"] = device.screenWidth;
obj["screenHeight"] = device.screenHeight;
obj["battery"] = device.battery;
list.append(obj);
}
jsonPart.setBody(QJsonDocument(list).toJson());
multiPart->append(jsonPart);
for (const DeviceInfo &device: devices) {
QHttpPart imagePart;
QString imageHeader = QStringLiteral("form-data; name=files; filename=%1.jpg").arg(device.id);
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, imageHeader.toUtf8());
imagePart.setBody(device.image);
multiPart->append(imagePart);
}
QHttpPart uniquePart;
uniquePart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="uniqueName")");
uniquePart.setBody("DT88bokcwQ");
multiPart->append(uniquePart);
QNetworkAccessManager manager; // создаём внутри потока
QUrl url("http://localhost:9999/api/v1/app/update");
QNetworkRequest request(url);
// делаем запрос
QNetworkReply *reply = manager.post(request, multiPart);
multiPart->setParent(reply); // multiPart удалится, когда придёт ответ
// локальный цикл — ждём, пока придёт finished()
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QObject::connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit();
});
loop.exec(); // <- здесь и запускается event-loop потока
// обрабатываем результат
if (reply->error() == QNetworkReply::NoError) {
QByteArray raw = reply->readAll();
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject root = doc.object();
qDebug() << "Успешный ответ: " << root.value("success").toBool();
}
} else {
qWarning() << "Ошибка:" << reply->errorString();
}
reply->deleteLater();
}
void DeviceScreener::start() {
qDebug() << "Worker started in thread:" << QThread::currentThread();
while (m_running) {
QList<QPair<QString, QString> > devices = readAdbDevices();
QStringList onlineIds;
QList<DeviceInfo> deviceInfos;
for (const auto &[id, status]: devices) {
qDebug() << "Id:" << id << ", Status:" << status;
DeviceInfo device;
device.id = id;
if (status == "device") {
onlineIds << device.id;
device = readDeviceInfo(id);
takeScreenshot(id);
device.image = takeScreenshot(id);
if (!DeviceDAO::updateImage(id, device.image)) {
qDebug() << "Cant update screenshot";
}
}
device.status = status;
device.updateTime = QDateTime::currentDateTime();
qDebug() << "Update device: " << DeviceDAO::upsertDevice(device);
deviceInfos.append(device);
if (!DeviceDAO::upsertDevice(device)) {
qDebug() << "Cant update devive";
}
}
postDevicesData(deviceInfos);
if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) {
qDebug() << "Cant mark devices offline";
}
qDebug() << "All devices offline: " << DeviceDAO::markDevicesOfflineExcept(onlineIds);
QThread::sleep(3);
}
emit finished();
@ -47,11 +133,6 @@ QByteArray convertPngToJpeg(const QByteArray& pngData, const int quality = 90) {
return {};
}
// if (!image.save("test.jpg", "JPG", quality)) {
// qWarning() << "Не удалось сохранить как JPG:" << "test.jpg";
// return {};
// }
QByteArray jpgData;
QBuffer buffer(&jpgData);
buffer.open(QIODevice::WriteOnly);
@ -59,7 +140,7 @@ QByteArray convertPngToJpeg(const QByteArray& pngData, const int quality = 90) {
return jpgData;
}
bool DeviceScreener::takeScreenshot(const QString& deviceId) {
QByteArray DeviceScreener::takeScreenshot(const QString &deviceId) {
QProcess process;
QStringList arguments;
@ -68,24 +149,17 @@ bool DeviceScreener::takeScreenshot(const QString& deviceId) {
if (!process.waitForFinished(5000)) {
qWarning() << "ADB скриншот не завершился для" << deviceId;
return false;
return {};
}
const QByteArray imageData = process.readAllStandardOutput();
if (imageData.isEmpty()) {
qWarning() << "Скриншот пустой для " << deviceId;
return false;
return {};
}
// Обновляем изображение в БД
if (!DeviceDAO::updateImage(deviceId, convertPngToJpeg(imageData, 90))) {
qWarning() << "Не удалось записать изображение в БД для" << deviceId;
return false;
}
qDebug() << "Screenshot saved, device: " << deviceId;
return true;
return convertPngToJpeg(imageData, 90);
}
QList<QPair<QString, QString> > DeviceScreener::readAdbDevices() {
@ -94,7 +168,6 @@ QList<QPair<QString, QString>> DeviceScreener::readAdbDevices() {
process.waitForFinished();
const QString output = process.readAllStandardOutput();
// qDebug() << "[adb devices output]:\n" << output;
QList<QPair<QString, QString> > devices;
const QStringList lines = output.split('\n');
for (int i = 1; i < lines.size(); ++i) {

View File

@ -24,7 +24,7 @@ private:
static QList<QPair<QString, QString> > readAdbDevices();
static bool takeScreenshot(const QString &deviceId);
static QByteArray takeScreenshot(const QString &deviceId);
static DeviceInfo readDeviceInfo(const QString &deviceId);
};

View File

@ -0,0 +1,69 @@
#include "NetworkService.h"
#include <QEventLoop>
#include <QHttpPart>
#include <QNetworkReply>
#include <QJsonObject>
NetworkService::NetworkService(QObject *parent) : QObject(parent) {
}
NetworkService::~NetworkService() = default;
void NetworkService::postAccountsData() {
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart jsonPart;
jsonPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/json; charset=UTF-8");
jsonPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="metadata")");
jsonPart.setBody(m_json);
multiPart->append(jsonPart);
QHttpPart desktopUniqueName;
desktopUniqueName.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="desktopUniqueName")");
desktopUniqueName.setBody(m_desktopId.toUtf8());
multiPart->append(desktopUniqueName);
QHttpPart androidUniqueName;
androidUniqueName.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="androidUniqueName")");
androidUniqueName.setBody(m_deviceId.toUtf8());
multiPart->append(androidUniqueName);
QNetworkAccessManager manager; // создаём внутри потока
const QUrl url("http://localhost:9999/api/v1/account/update");
const QNetworkRequest request(url);
QNetworkReply *reply = manager.post(request, multiPart);
multiPart->setParent(reply);
QEventLoop loop;
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit();
});
loop.exec(); // <- здесь и запускается event-loop потока
// обрабатываем результат
if (reply->error() == QNetworkReply::NoError) {
const QByteArray raw = reply->readAll();
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
const QJsonObject root = doc.object();
qDebug() << "Успешный ответ: " << root.value("success").toBool();
}
} else {
qWarning() << "Ошибка:" << reply->errorString();
}
reply->deleteLater();
emit finished();
}
void NetworkService::stop() {
m_running = false;
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <QObject>
class NetworkService final : public QObject {
Q_OBJECT
public:
explicit NetworkService(QObject *parent = nullptr);
~NetworkService() override;
void setPayload(const QByteArray &json) { m_json = json; }
void setDeviceData(const QString &desktopId, const QString &deviceId) {
m_desktopId = desktopId;
m_deviceId = deviceId;
}
public slots:
void postAccountsData();
void stop();
signals:
void finished();
private:
QByteArray m_json;
QString m_deviceId;
QString m_desktopId;
bool m_running = true;
};

View File

@ -30,7 +30,7 @@ AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) {
QList<DeviceInfo> devices = DeviceDAO::getAllDevices(false);
for (int i = 0; i < devices.size(); ++i) {
DeviceInfo device = devices[i];
QList<AccountInfo> accounts = AccountDAO::getAccounts(device.id, "bank");
QList<AccountInfo> accounts = AccountDAO::getAccounts(device.id, "rshb");
// Текст перед первой таблицей
QString title = QString("%1 [%2] | %3").arg(device.name, device.id, device.status);
QLabel *label = new QLabel(title, this);

View File

@ -13,7 +13,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
img.loadFromData(device.image, "JPG");
auto *imageLabel = new QLabel(this);
qDebug() << "imageLabel" << img.width() ;
// qDebug() << "imageLabel" << img.width() ;
imageLabel->setPixmap(QPixmap::fromImage(img).scaled(200, 350, Qt::KeepAspectRatio, Qt::SmoothTransformation));
imageLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(imageLabel);

View File

@ -21,12 +21,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
createDevicePage();
stackedWidget->addWidget(devicePage);
// stackedWidget->setCurrentWidget(devicePage); FIXME
stackedWidget->setCurrentWidget(devicePage);
// FIXME переделать на ленивое создание
accountPage = new AccountWindow(this);
stackedWidget->addWidget(accountPage);
stackedWidget->setCurrentWidget(accountPage);
// stackedWidget->setCurrentWidget(accountPage);
resize(800, 600);

View File

@ -1,21 +1,159 @@
#include "TransactionWindow.h"
#include <qabstractitemview.h>
#include <QHeaderView>
#include <QVBoxLayout>
#include <QLabel>
#include <QScrollArea>
#include <QTableWidget>
#include <QStyledItemDelegate>
#include "dao/AccountDAO.h"
#include "dao/TransactionDAO.h"
class AmountDelegate : public QStyledItemDelegate {
public:
explicit AmountDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {
}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
// Получаем значение суммы
QString text = index.data().toString();
bool isNegative = text.toDouble() < 0;
// Настраиваем цвета
if (isNegative) {
opt.backgroundBrush = QColor(255, 230, 230); // Светло-красный фон
opt.palette.setColor(QPalette::Text, QColor(255, 0, 0)); // Красный текст
} else {
opt.backgroundBrush = QColor(230, 255, 230); // Светло-зеленый фон
opt.palette.setColor(QPalette::Text, QColor(0, 128, 0)); // Зеленый текст
}
// Рисуем ячейку
QStyledItemDelegate::paint(painter, opt, index);
}
};
QTableWidgetItem *createTableWidget(const QString &text) {
QTableWidgetItem *item = new QTableWidgetItem(text);
item->setTextAlignment(Qt::AlignLeft | Qt::AlignTop);
return item;
}
TransactionWindow::TransactionWindow(const int accountId, QWidget *parent): QDialog(parent) {
setWindowTitle("Транзакции");
setMinimumSize(300, 200); // минимальный размер
setMaximumSize(800, 400); // максимальный размер
setMinimumSize(1000, 600); // минимальный размер
setMaximumSize(1000, 600); // максимальный размер
auto *layout = new QVBoxLayout(this);
layout->setAlignment(Qt::AlignCenter);
auto *mainLayout = new QVBoxLayout(this);
// Скролл-область
auto *scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
// Виджет-контейнер, в котором будет layout с таблицами и лейблами
auto *contentWidget = new QWidget;
auto *layout = new QVBoxLayout(contentWidget);
// Убираем отступы и рамку
contentWidget->setContentsMargins(0, 0, 0, 0); // Убираем отступы
layout->setContentsMargins(0, 0, 0, 0); // Убираем отступы у layout
scrollArea->setStyleSheet("border: none;"); // Убираем рамку
layout->setAlignment(Qt::AlignTop);
AccountInfo account = AccountDAO::getAccountById(accountId);
QList<TransactionInfo> transactions = TransactionDAO::getTransactions(accountId);
QLabel *label = new QLabel(QString("*%1 | %2").arg(account.lastNumbers, account.description), this);
layout->addWidget(label);
QTableWidget *tableWidget = new QTableWidget(this);
tableWidget->setStyleSheet(
"QTableWidget {"
" border: 1px solid grey;"
" gridline-color: lightgrey;"
" background-color: #f9f9f9;"
" font-size: 14px;"
"}"
"QHeaderView::section {"
" background-color: #e0e0e0;"
" border: 1px solid grey;"
" padding: 4px;"
" font-weight: bold;"
" text-align: left center;"
"}"
"QTableWidget::item {"
" padding: 4px;"
" border: none;"
" text-align: left top;"
"}"
"QTableWidget::item:selected {"
" background-color: #cce5ff;"
" color: #003366;"
"}"
"QTableCornerButton::section {"
" background-color: #e0e0e0;"
" border: 1px solid grey;"
"}"
);
tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
tableWidget->setRowCount(transactions.length());
tableWidget->setColumnCount(6);
tableWidget->setHorizontalHeaderLabels({
"Дата и время",
"Сумма",
"Банк",
"Телефон",
"Статус",
"Описание"
});
tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
for (int i = 0; i < 5; ++i) {
tableWidget->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
}
tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch);
// tableWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
tableWidget->setItemDelegateForColumn(1, new AmountDelegate(tableWidget));
for (int k = 0; k < transactions.size(); ++k) {
TransactionInfo tr = transactions[k];
tableWidget->setItem(k, 0, createTableWidget(tr.timestamp.toString("dd.MM.yyyy HH:mm:ss")));
tableWidget->setItem(k, 1, createTableWidget(QString::number(tr.amount)));
tableWidget->setItem(k, 2, createTableWidget(tr.bankName));
tableWidget->setItem(k, 3, createTableWidget(tr.phone));
tableWidget->setItem(k, 4, createTableWidget(transactionStatusToString(tr.status)));
QTableWidgetItem *item = new QTableWidgetItem(tr.description);
item->setTextAlignment(Qt::AlignLeft | Qt::AlignTop);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
item->setSizeHint(QSize(100, tr.description.length() / 2)); // Минимальная высота ячейки для переноса
tableWidget->setItem(k, 5, item);
}
layout->addWidget(tableWidget);
tableWidget->resizeRowsToContents();
// for (int row = 0; row < tableWidget->rowCount(); ++row) {
// for (int col = 0; col < tableWidget->columnCount(); ++col) {
// QTableWidgetItem *item = tableWidget->item(row, col);
// if (item) {
// item->setTextAlignment(Qt::AlignLeft | Qt::AlignTop);
// }
// }
// }
// Добавим вертикальный Spacer в конец, чтобы прокручиваемая область не растягивала контент
// QSpacerItem *spacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding);
// layout->addItem(spacer);
contentWidget->setLayout(layout);
scrollArea->setWidget(contentWidget);
mainLayout->addWidget(scrollArea);
setLayout(mainLayout);
setWindowTitle("Транзакции");
setModal(true);
}