1
0
forked from BRT/arc

Remove AccountDAO, ApplicationDAO, and related models, replacing them with BankProfileDAO and BankProfileInfo.

This commit is contained in:
slava 2026-03-31 00:11:50 +07:00
parent b395ed41a5
commit bceceb516b
70 changed files with 1951 additions and 811 deletions

View File

@ -8,7 +8,8 @@
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/BankProfileDAO.h"
#include "net/NetworkService.h"
namespace Black {
@ -108,7 +109,7 @@ bool CommonScript::runAppAndGoToHomeScreen(
}
}
void CommonScript::postAccountsData(const QList<QPair<AccountInfo, Node>> &accounts) {
void CommonScript::postAccountsData(const QList<QPair<MaterialInfo, Node>> &accounts) {
if (accounts.isEmpty()) {
return;
}
@ -117,15 +118,15 @@ void CommonScript::postAccountsData(const QList<QPair<AccountInfo, Node>> &accou
auto *worker = new NetworkService;
QJsonArray list;
for (const QPair<AccountInfo, Node> &pair : accounts) {
for (const QPair<MaterialInfo, Node> &pair : accounts) {
QJsonObject obj;
AccountInfo account = pair.first;
MaterialInfo account = pair.first;
obj["appName"] = account.appCode;
obj["lastNumbers"] = account.lastNumbers;
obj["amount"] = account.amount;
obj["description"] = account.description;
obj["currency"] = account.currency;
obj["status"] = accountStatusToString(account.status);
obj["status"] = materialStatusToString(account.status);
list.append(obj);
}
@ -139,7 +140,7 @@ void CommonScript::postAccountsData(const QList<QPair<AccountInfo, Node>> &accou
thread->start();
}
void CommonScript::postNewTransactionData(const AccountInfo &account, const TransactionInfo &transaction) {
void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) {
auto *thread = new QThread;
auto *worker = new NetworkService;
@ -170,7 +171,7 @@ void CommonScript::postNewTransactionData(const AccountInfo &account, const Tran
}
void CommonScript::updateTransactionData(
const AccountInfo &account,
const MaterialInfo &account,
const int transactionId,
const TransactionStatus status,
const QString &oldDesc,
@ -197,7 +198,7 @@ void CommonScript::updateTransactionData(
}
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) {
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, appCode);
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
qWarning() << "[Black] createBankProfileIfNeeded: skipping — fullName or phone is empty";
@ -224,6 +225,8 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
}
phone.prepend('+');
const DeviceInfo device = DeviceDAO::getDeviceById(deviceId);
QJsonObject profileBody;
profileBody["first_name"] = parts.value(0);
profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString("");
@ -232,6 +235,7 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
profileBody["phone_number"] = phone;
profileBody["balance"] = QString::number(qRound64(app.amount * 100));
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
profileBody["device_id"] = device.apiId;
profileBody["currency"] = currencyCode;
NetworkService ns;

View File

@ -34,12 +34,12 @@ protected:
int &counter, int width, int height
);
void postAccountsData(const QList<QPair<AccountInfo, Node>> &accounts);
void postAccountsData(const QList<QPair<MaterialInfo, Node>> &accounts);
void postNewTransactionData(const AccountInfo &account, const TransactionInfo &transaction);
void postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction);
void updateTransactionData(
const AccountInfo &account,
const MaterialInfo &account,
int transactionId,
TransactionStatus status,
const QString &oldDesc,

View File

@ -6,8 +6,8 @@
#include <QJsonObject>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "net/NetworkService.h"
@ -25,8 +25,8 @@ GetCardInfoScript::GetCardInfoScript(
GetCardInfoScript::~GetCardInfoScript() = default;
void GetCardInfoScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
@ -41,7 +41,7 @@ void GetCardInfoScript::doStart() {
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[Black::GetCardInfo] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode;
qWarning() << m_error;
@ -55,7 +55,7 @@ void GetCardInfoScript::doStart() {
// 1. Парсим баланс с домашнего экрана
const double balance = xmlScreenParser.parseBalanceFromHomeScreen();
qDebug() << "[Black::GetCardInfo] balance from home:" << balance;
ApplicationDAO::updateAmount(app.id, balance);
BankProfileDAO::updateAmount(app.id, balance);
// 2. Нажимаем "Tu CVU" на домашнем экране
Node cvuButton = xmlScreenParser.findButtonNode("Tu CVU", false);
@ -104,24 +104,24 @@ void GetCardInfoScript::doStart() {
// 5. Сохраняем аккаунт в БД (lastNumbers = последние 4 цифры CVU)
const QString lastNumbers = cvuNumber.right(4);
AccountInfo account;
account.deviceId = m_deviceId;
MaterialInfo account;
account.deviceId = device.id;
account.appCode = m_appCode;
account.lastNumbers = lastNumbers;
account.cardNumber = cvuNumber;
account.amount = balance;
account.currency = "USD";
account.description = "CVU";
account.status = AccountStatus::Active;
account.status = MaterialStatus::Active;
account.updateTime = QDateTime::currentDateTimeUtc();
if (!AccountDAO::upsertAccount(account)) {
if (!MaterialDAO::upsertAccount(account)) {
qWarning() << "[Black::GetCardInfo] Failed to upsert account";
}
// 6. Отправляем материал на сервер (если ещё нет material_id)
const AccountInfo savedAccount = AccountDAO::findAppAccount(m_deviceId, m_appCode, lastNumbers);
const ApplicationInfo updatedApp = ApplicationDAO::getApplication(m_deviceId, m_appCode);
const MaterialInfo savedAccount = MaterialDAO::findAppAccount(device.id, m_appCode, lastNumbers);
const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode);
if (savedAccount.id != -1 && !updatedApp.bankProfileId.isEmpty()) {
if (savedAccount.materialId.isEmpty()) {

View File

@ -19,7 +19,7 @@ protected:
void doStart() override;
private:
const QString m_deviceId;
QString m_deviceId;
const QString m_appCode;
QString m_error;
};

View File

@ -4,8 +4,8 @@
#include <QTimeZone>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "AppLogger.h"
@ -23,8 +23,8 @@ GetLastTransactionsScript::GetLastTransactionsScript(
GetLastTransactionsScript::~GetLastTransactionsScript() = default;
void GetLastTransactionsScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
@ -38,7 +38,7 @@ void GetLastTransactionsScript::doStart() {
// Если не на домашнем экране — перезапускаем приложение
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[Black::GetLastTransactions] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId;
qWarning() << m_error;
@ -113,7 +113,7 @@ void GetLastTransactionsScript::doStart() {
// Находим accountId для сохранения транзакций
int accountId = -1;
const QList<AccountInfo> accounts = AccountDAO::getAccounts(m_deviceId, m_appCode);
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(device.id, m_appCode);
if (!accounts.isEmpty()) {
accountId = accounts.first().id;
qDebug() << "[Black::GetLastTransactions] Using accountId:" << accountId;

View File

@ -19,7 +19,7 @@ protected:
void doStart() override;
private:
const QString m_deviceId;
QString m_deviceId;
const QString m_appCode;
QString m_error;
};

View File

@ -5,8 +5,8 @@
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "net/NetworkService.h"
@ -24,8 +24,8 @@ GetProfileInfoScript::GetProfileInfoScript(
GetProfileInfoScript::~GetProfileInfoScript() = default;
void GetProfileInfoScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
@ -40,7 +40,7 @@ void GetProfileInfoScript::doStart() {
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[Black::GetProfileInfo] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode;
qWarning() << m_error;
@ -67,7 +67,7 @@ void GetProfileInfoScript::doStart() {
qDebug() << "[Black::GetProfileInfo] fullName from home:" << fullName << "balance:" << balance;
// Сохраняем баланс
ApplicationDAO::updateAmount(app.id, balance);
BankProfileDAO::updateAmount(app.id, balance);
AdbUtils::makeTap(m_deviceId, holaNode.x(), holaNode.y());
QThread::msleep(2000);
@ -155,7 +155,7 @@ void GetProfileInfoScript::doStart() {
<< "fullName:" << parsedFullName;
// 7. Сохраняем в БД
if (!ApplicationDAO::updateProfileInfo(app.id, parsedFullName, phone, email)) {
if (!BankProfileDAO::updateProfileInfo(app.id, parsedFullName, phone, email)) {
qWarning() << "[Black::GetProfileInfo] Failed to save profile info to DB";
}
@ -163,8 +163,8 @@ void GetProfileInfoScript::doStart() {
createBankProfileIfNeeded(m_deviceId, m_appCode);
// 8. Отправляем данные аккаунтов на сервер если нет material_id
const QList<AccountInfo> allAccounts = AccountDAO::getAccounts(m_deviceId, m_appCode);
QList<QPair<AccountInfo, Node>> accountsToPost;
const QList<MaterialInfo> allAccounts = MaterialDAO::getAccounts(device.id, m_appCode);
QList<QPair<MaterialInfo, Node>> accountsToPost;
for (const auto &acc : allAccounts) {
if (acc.materialId.isEmpty()) {
accountsToPost.append({acc, Node()});

View File

@ -19,7 +19,7 @@ protected:
void doStart() override;
private:
const QString m_deviceId;
QString m_deviceId;
const QString m_appCode;
QString m_error;
};

View File

@ -3,7 +3,7 @@
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/ApplicationDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "AppLogger.h"
#include "GetProfileInfoScript.h"
@ -22,8 +22,9 @@ LoginAndCheckAccountsScript::LoginAndCheckAccountsScript(
LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default;
void LoginAndCheckAccountsScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
// m_deviceId = ADB serial, device.id = android_id (для БД)
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
@ -34,26 +35,20 @@ void LoginAndCheckAccountsScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) {
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) {
m_error = "Cant open the home screen: " + m_deviceId + " " + m_appCode;
qWarning() << m_error;
AppLogger::log("black/LoginAndCheck", m_deviceId, m_error,
AdbUtils::captureScreenshotBytes(m_deviceId),
"FETCH_PROFILE");
if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) {
qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode;
}
BankProfileDAO::updateComment(app.id,
"Не прошла проверка " + QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm:ss"));
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
// PIN-код прошёл
if (!ApplicationDAO::updatePinCodeStatus(app.id, "yes", "")) {
qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode;
}
// Проверяем профиль и аккаунты (приложение уже на home screen)
GetProfileInfoScript profileScript(m_deviceId, m_appCode);
profileScript.start();

View File

@ -19,7 +19,7 @@ protected:
void doStart() override;
private:
const QString m_deviceId;
QString m_deviceId;
const QString m_appCode;
QString m_error;
};

View File

@ -4,7 +4,7 @@
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/ApplicationDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "AppLogger.h"
@ -12,7 +12,7 @@
namespace Black {
PayByCardScript::PayByCardScript(
AccountInfo account,
MaterialInfo account,
QString cardNumber,
double amount,
QObject *parent
@ -26,7 +26,7 @@ PayByCardScript::~PayByCardScript() = default;
void PayByCardScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode);
const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_account.deviceId;
@ -35,7 +35,7 @@ void PayByCardScript::doStart() {
return;
}
makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight);
makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}
@ -50,7 +50,7 @@ void PayByCardScript::makePayment(
// 0. Убеждаемся что на домашнем экране
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[Black::PayByCard] Not on home screen, restarting app...";
const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode);
const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode);
if (!runAppAndGoToHomeScreen(deviceId, app.package, pinCode, width, height)) {
m_error = "Cannot reach home screen: " + deviceId;
qWarning() << m_error;

View File

@ -8,7 +8,7 @@ class PayByCardScript final : public CommonScript {
public:
PayByCardScript(
AccountInfo account,
MaterialInfo account,
QString cardNumber,
double amount,
QObject *parent = nullptr
@ -20,7 +20,7 @@ protected:
void doStart() override;
private:
const AccountInfo m_account;
const MaterialInfo m_account;
const QString m_cardNumber;
const double m_amount;
QString m_error;

View File

@ -3,7 +3,7 @@
#include <QObject>
#include <QDomElement>
#include "android/xml/Node.h"
#include "db/AccountInfo.h"
#include "db/MaterialInfo.h"
#include "db/TransactionInfo.h"
namespace Black {

View File

@ -8,7 +8,8 @@
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/BankProfileDAO.h"
#include "net/NetworkService.h"
namespace Ozon {
@ -113,11 +114,18 @@ bool CommonScript::runAppAndGoToHomeScreen(
continue;
}
// Проверяем на рекламный bottom sheet (ипотека и т.п.)
// Проверяем на рекламный bottom sheet (ипотека, открыть счёт и т.п.)
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[runAppAndGoToHomeScreen] Ad bottom sheet detected, closing...";
AdbUtils::makeTap(deviceId, adNode.x(), adNode.y());
QThread::msleep(1000);
// Если touch_outside не помог — жмём назад
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
if (Node stillAd = xmlScreenParser.tryToFindAdBanner(); !stillAd.isEmpty()) {
qDebug() << "[runAppAndGoToHomeScreen] Ad still visible, pressing back...";
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
}
continue;
}
@ -187,7 +195,7 @@ bool CommonScript::goToAllProducts(
}
bool CommonScript::findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers) {
QList<QPair<AccountInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
QList<QPair<MaterialInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
for (auto &[account, node]: accounts) {
// FIXME будем считать что числа последние везде уникальные
if (accountNumbers == account.lastNumbers) {
@ -201,7 +209,7 @@ bool CommonScript::findAndTapOnAccount(const QString &deviceId, const QString &a
}
void CommonScript::postAccountsData(const QList<QPair<AccountInfo, Node> > &accounts) {
void CommonScript::postAccountsData(const QList<QPair<MaterialInfo, Node> > &accounts) {
if (accounts.isEmpty()) {
return;
}
@ -210,15 +218,15 @@ void CommonScript::postAccountsData(const QList<QPair<AccountInfo, Node> > &acco
auto *worker = new NetworkService;
QJsonArray list;
for (const QPair<AccountInfo, Node> &pair: accounts) {
for (const QPair<MaterialInfo, Node> &pair: accounts) {
QJsonObject obj;
AccountInfo account = pair.first;
MaterialInfo account = pair.first;
obj["appName"] = account.appCode;
obj["lastNumbers"] = account.lastNumbers;
obj["amount"] = account.amount;
obj["description"] = account.description;
obj["currency"] = account.currency;
obj["status"] = accountStatusToString(account.status);
obj["status"] = materialStatusToString(account.status);
list.append(obj);
}
@ -233,7 +241,7 @@ void CommonScript::postAccountsData(const QList<QPair<AccountInfo, Node> > &acco
thread->start();
}
void CommonScript::postNewTransactionData(const AccountInfo &account, const TransactionInfo &transaction) {
void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) {
auto *thread = new QThread;
auto *worker = new NetworkService;
@ -265,7 +273,7 @@ void CommonScript::postNewTransactionData(const AccountInfo &account, const Tran
}
void CommonScript::updateTransactionData(
const AccountInfo &account,
const MaterialInfo &account,
const int transactionId,
const TransactionStatus status,
const QString &oldDesc,
@ -293,7 +301,7 @@ void CommonScript::updateTransactionData(
}
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) {
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, appCode);
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
qWarning() << "[Ozon] createBankProfileIfNeeded: skipping — fullName or phone is empty";
@ -320,6 +328,8 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
}
phone.prepend('+');
const DeviceInfo device = DeviceDAO::getDeviceById(deviceId);
QJsonObject profileBody;
profileBody["first_name"] = parts.value(0);
profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString("");
@ -328,6 +338,7 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
profileBody["phone_number"] = phone;
profileBody["balance"] = QString::number(qRound64(app.amount * 100));
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
profileBody["device_id"] = device.apiId;
profileBody["currency"] = currencyCode;
NetworkService ns;

View File

@ -27,12 +27,12 @@ protected:
bool findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers);
void postAccountsData(const QList<QPair<AccountInfo, Node> > &accounts);
void postAccountsData(const QList<QPair<MaterialInfo, Node> > &accounts);
void postNewTransactionData(const AccountInfo &account, const TransactionInfo &transaction);
void postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction);
void updateTransactionData(
const AccountInfo &account,
const MaterialInfo &account,
int transactionId,
TransactionStatus status,
const QString &oldDesc,

View File

@ -7,8 +7,8 @@
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/SettingsDAO.h"
#include "net/NetworkService.h"
@ -30,8 +30,8 @@ GetCardInfoScript::GetCardInfoScript(
GetCardInfoScript::~GetCardInfoScript() = default;
void GetCardInfoScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
@ -57,7 +57,7 @@ void GetCardInfoScript::doStart() {
// Если не на домашнем экране — перезапускаем приложение
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[GetCardInfo] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId;
qWarning() << m_error;
@ -97,7 +97,7 @@ void GetCardInfoScript::doStart() {
const QString fullName = xmlScreenParser.parseProfileFullName();
const QString phone = xmlScreenParser.parseProfilePhone();
qDebug() << "[GetCardInfo] Profile parsed - fullName:" << fullName << "phone:" << phone;
ApplicationDAO::updateProfileInfo(app.id, fullName, phone);
BankProfileDAO::updateProfileInfo(app.id, fullName, phone);
} else {
qWarning() << "[GetCardInfo] Profile screen not reached, skipping profile parse";
}
@ -113,7 +113,7 @@ void GetCardInfoScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[GetCardInfo] Not on home screen before card search, restarting...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen before card search: " + m_deviceId;
qWarning() << m_error;
@ -129,7 +129,7 @@ void GetCardInfoScript::doStart() {
}
// Ждём пока все карты подгрузятся — до 5 попыток
QList<AccountInfo> cards;
QList<MaterialInfo> cards;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
cards = xmlScreenParser.parseCardsOnHomeScreen();
@ -149,12 +149,12 @@ void GetCardInfoScript::doStart() {
qDebug() << "[GetCardInfo] Found" << cards.size() << "card(s) on home screen";
// Апсёртим аккаунты для всех найденных карт
for (AccountInfo &card : cards) {
card.deviceId = m_deviceId;
for (MaterialInfo &card : cards) {
card.deviceId = device.id;
card.appCode = m_appCode;
card.updateTime = QDateTime::currentDateTimeUtc();
card.status = AccountStatus::Active;
if (!AccountDAO::upsertAccount(card)) {
card.status = MaterialStatus::Active;
if (!MaterialDAO::upsertAccount(card)) {
qWarning() << "[GetCardInfo] Failed to upsert account for card" << card.lastNumbers;
} else {
qDebug() << "[GetCardInfo] Upserted account:" << card.lastNumbers
@ -165,7 +165,7 @@ void GetCardInfoScript::doStart() {
// Сохраняем баланс домашнего экрана в application
const double totalAmount = cards.isEmpty() ? 0.0 : cards[0].amount;
if (totalAmount > 0.0) {
ApplicationDAO::updateAmount(app.id, totalAmount);
BankProfileDAO::updateAmount(app.id, totalAmount);
}
// Синхронная отправка на сервер (NetworkService блокируется через QEventLoop внутри)
@ -188,7 +188,7 @@ void GetCardInfoScript::doStart() {
// 2. Отправляем банковский профиль (используем свежие данные из БД — имя/телефон уже спарсены)
{
const ApplicationInfo freshApp = ApplicationDAO::getApplication(m_deviceId, m_appCode);
const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode);
const QStringList parts = freshApp.fullName.split(' ', Qt::SkipEmptyParts);
// ISO 4217 numeric currency codes
@ -207,6 +207,7 @@ void GetCardInfoScript::doStart() {
profileBody["phone_number"] = phone;
profileBody["balance"] = QString::number(qRound64(totalAmount * 100));
profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled";
profileBody["device_id"] = device.apiId;
profileBody["currency"] = currencyCode;
ns.setPayload(QJsonDocument(profileBody).toJson());
@ -214,7 +215,7 @@ void GetCardInfoScript::doStart() {
ns.addExtra("app_id", freshApp.id);
ns.postBankProfile();
// Проверяем что bankProfileId сохранился
const ApplicationInfo afterPost = ApplicationDAO::getApplication(m_deviceId, m_appCode);
const BankProfileInfo afterPost = BankProfileDAO::getApplication(device.id, m_appCode);
if (afterPost.bankProfileId.isEmpty()) {
const QString err = "postBankProfile failed for " + m_deviceId;
qWarning() << "[GetCardInfo]" << err;
@ -233,7 +234,7 @@ void GetCardInfoScript::doStart() {
// Для каждой карты переходим на её экран и получаем полный номер через OCR
for (int i = 0; i < cards.size(); ++i) {
const AccountInfo &card = cards[i];
const MaterialInfo &card = cards[i];
// Если передан конкретный lastNumbers — обрабатываем только его
if (!m_lastNumbers.isEmpty() && card.lastNumbers != m_lastNumbers) continue;
@ -241,7 +242,7 @@ void GetCardInfoScript::doStart() {
qDebug() << "[GetCardInfo] Getting card number for" << card.lastNumbers;
// Если номер карты уже есть в БД — сумма уже обновлена через upsert, навигация не нужна
AccountInfo existing = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers);
MaterialInfo existing = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers);
if (existing.id != -1 && !existing.cardNumber.isEmpty()) {
qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "already has number, skipping";
continue;
@ -339,16 +340,16 @@ void GetCardInfoScript::doStart() {
if (!cardNumber.isEmpty()) {
qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "full number:" << cardNumber;
// Получаем id аккаунта из БД и сохраняем номер карты
AccountInfo saved = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers);
MaterialInfo saved = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers);
if (saved.id != -1) {
AccountDAO::updateCardNumber(saved.id, cardNumber);
MaterialDAO::updateCardNumber(saved.id, cardNumber);
}
} else {
const QString err = "Card number not found for " + card.lastNumbers;
qWarning() << "[GetCardInfo]" << err;
AccountInfo saved = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers);
MaterialInfo saved = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers);
if (saved.id != -1) {
AccountDAO::updateAccountById(saved.id, accountStatusToString(AccountStatus::Off),
MaterialDAO::updateAccountById(saved.id, materialStatusToString(MaterialStatus::Off),
saved.description, saved.currency);
}
AppLogger::log("ozon/GetCardInfo", m_deviceId, err,
@ -365,10 +366,10 @@ void GetCardInfoScript::doStart() {
}
// 3. Отправляем материалы (карты) — после OCR, когда номера карт уже в БД
const ApplicationInfo updatedApp = ApplicationDAO::getApplication(m_deviceId, m_appCode);
const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode);
if (!updatedApp.bankProfileId.isEmpty()) {
for (const AccountInfo &card : cards) {
const AccountInfo dbCard = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers);
for (const MaterialInfo &card : cards) {
const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers);
if (dbCard.cardNumber.isEmpty()) continue;
if (!dbCard.materialId.isEmpty()) {
qDebug() << "[GetCardInfo] material already exists for card" << dbCard.lastNumbers << ", skipping";
@ -379,7 +380,7 @@ void GetCardInfoScript::doStart() {
materialBody["bank_profile_id"] = updatedApp.bankProfileId;
materialBody["card_number"] = dbCard.cardNumber;
materialBody["name"] = updatedApp.fullName;
materialBody["state"] = accountStatusToString(dbCard.status) == "active" ? "enabled" : "disabled";
materialBody["state"] = materialStatusToString(dbCard.status) == "active" ? "enabled" : "disabled";
ns.setPayload(QJsonDocument(materialBody).toJson());
ns.addExtra("account_id", dbCard.id);
ns.postMaterial();

View File

@ -23,7 +23,7 @@ protected:
void doStart() override;
private:
const QString m_deviceId;
QString m_deviceId;
const QString m_appCode;
const QString m_lastNumbers;
QString m_error;

View File

@ -9,8 +9,8 @@
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "time/DateUtils.h"
@ -33,7 +33,6 @@ static QString recognizeTextFromImage(const QByteArray &pngData) {
return {};
}
// Convert to grayscale for better OCR accuracy
Pix *gray = pixConvertRGBToGray(pix, 0.0f, 0.0f, 0.0f);
pixDestroy(&pix);
if (!gray) {
@ -44,8 +43,6 @@ static QString recognizeTextFromImage(const QByteArray &pngData) {
}
api->SetImage(gray);
// adb screencap PNGs lack DPI metadata; set 300 DPI so Tesseract
// interprets character sizes correctly
api->SetSourceResolution(300);
char *text = api->GetUTF8Text();
QString result = QString::fromUtf8(text).trimmed();
@ -58,12 +55,9 @@ static QString recognizeTextFromImage(const QByteArray &pngData) {
}
static QString extractTransactionId(const QString &ocrText) {
// Ищем по слову "операции" — OCR часто искажает "ID" перед ним
// \S{1,3} вместо точного "ID" покрывает любые OCR-ошибки (1О, 1Ю, ID, 10 и т.д.)
QRegularExpression reId(R"(\S{1,3}\s+операции\s*=?\s*([A-Za-zА-Яа-яЁё0-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);
@ -76,16 +70,24 @@ GetLastTransactionsScript::GetLastTransactionsScript(
QString deviceId,
QString appCode,
int maxCount,
double searchAmount,
QDateTime searchTime,
QObject *parent
) : CommonScript(parent),
m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)), m_maxCount(maxCount) {
m_deviceId(std::move(deviceId)),
m_appCode(std::move(appCode)),
m_maxCount(maxCount),
m_searchAmount(searchAmount),
m_searchTime(std::move(searchTime)) {
}
GetLastTransactionsScript::~GetLastTransactionsScript() = default;
void GetLastTransactionsScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId);
if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
@ -94,22 +96,49 @@ void GetLastTransactionsScript::doStart() {
return;
}
if (!navigateToTransactionList(device, app)) {
emit finishedWithResult(m_error);
return;
}
// Находим accountId
int accountId = -1;
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(device.id, m_appCode);
if (!accounts.isEmpty()) {
accountId = accounts.first().id;
} else {
qWarning() << "[GetLastTransactions] No account found for" << device.id << m_appCode;
}
// Выбор режима: поиск конкретной транзакции или сканирование последних
if (m_searchAmount > 0) {
searchTransaction(device, accountId);
} else {
scanRecentTransactions(device, accountId);
}
emit finishedWithResult(m_error);
}
bool GetLastTransactionsScript::navigateToTransactionList(const DeviceInfo &device, const BankProfileInfo &app) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Закрываем рекламный bottom sheet, если открыт
// Закрываем рекламный bottom sheet
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[GetLastTransactions] Ad banner detected, closing...";
AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (Node stillAd = xmlScreenParser.tryToFindAdBanner(); !stillAd.isEmpty()) {
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
}
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Если не на домашнем экране — перезапускаем приложение
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[GetLastTransactions] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId;
qWarning() << m_error;
@ -117,8 +146,7 @@ void GetLastTransactionsScript::doStart() {
AdbUtils::captureScreenshotBytes(m_deviceId),
"FETCH_TRANSACTIONS");
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
return false;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
@ -128,13 +156,10 @@ void GetLastTransactionsScript::doStart() {
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Скроллим вниз до кнопки "Все" и тапаем по ней
// Кнопка "Все" на домашнем экране находится у границы навбара (y ≈ 2190),
// поэтому сначала скроллим вниз, чтобы она поднялась выше навигационной панели
const int navBarTop = device.screenHeight - (device.screenHeight / 10); // ~90% высоты экрана
// Скроллим до "Все" и тапаем
const int navBarTop = device.screenHeight - (device.screenHeight / 10);
Node allBtn;
for (int i = 0; i < 10; ++i) {
// Сначала скроллим, потом ищем
const int x = device.screenWidth / 2;
const int offset = device.screenHeight / 4;
AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - offset, x, offset);
@ -142,74 +167,159 @@ void GetLastTransactionsScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
allBtn = xmlScreenParser.findButtonNode("Все", false);
if (!allBtn.isEmpty() && allBtn.y() > 0 && allBtn.y() < navBarTop) {
break;
}
if (!allBtn.isEmpty() && allBtn.y() > 0 && allBtn.y() < navBarTop) break;
allBtn = {};
qDebug() << "[GetLastTransactions] 'Все' not found above navbar, attempt" << i + 1 << "/ 10";
}
if (allBtn.isEmpty()) {
m_error = "Button 'Все' not found after scrolling: " + m_deviceId;
m_error = "Button 'Все' not found: " + m_deviceId;
qWarning() << m_error;
emit finishedWithResult(m_error);
return;
return false;
}
qDebug() << "[GetLastTransactions] Found 'Все' at" << allBtn.x() << allBtn.y();
AdbUtils::makeTap(m_deviceId, allBtn.x(), allBtn.y());
QThread::msleep(2000);
// Ждём загрузки экрана "Операции"
bool loaded = false;
// Ждём экран "Операции"
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isTransactionListScreen()) {
loaded = true;
break;
}
qDebug() << "[GetLastTransactions] Waiting for transaction list screen, attempt" << attempt + 1 << "/ 10";
if (xmlScreenParser.isTransactionListScreen()) return true;
QThread::msleep(1500);
}
if (!loaded) {
m_error = "Transaction list screen not loaded: " + m_deviceId;
qWarning() << m_error;
emit finishedWithResult(m_error);
return;
return false;
}
// Ждём появления транзакций (могут подгружаться)
void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int accountId) {
const QTimeZone msk("Europe/Moscow");
const int maxScrolls = 15;
qDebug() << "[FetchTransactions] Searching: amount=" << m_searchAmount
<< "time=" << m_searchTime.toString(Qt::ISODate);
for (int scroll = 0; scroll < maxScrolls; ++scroll) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
const QList<Node> txButtons = xmlScreenParser.findTransactionButtons(20);
for (const Node &btn : txButtons) {
const TransactionInfo parsed = xmlScreenParser.parseTransactionButtonText(btn.text);
if (qAbs(qAbs(parsed.amount) - qAbs(m_searchAmount)) > 0.01) continue;
qDebug() << "[FetchTransactions] Amount match:" << parsed.amount
<< "at" << btn.x() << btn.y();
AdbUtils::makeTap(m_deviceId, btn.x(), btn.y());
QThread::msleep(2000);
// Ждём загрузки деталей
TransactionInfo detail;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
detail = xmlScreenParser.parseTransactionDetail();
if (!detail.bankTime.isEmpty()) break;
if (xmlScreenParser.isTransactionListScreen()) {
AdbUtils::makeTap(m_deviceId, btn.x(), btn.y());
}
QThread::msleep(1500);
}
if (detail.bankTime.isEmpty()) {
qWarning() << "[FetchTransactions] Could not load detail, going back";
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
continue;
}
// Парсим время
QString dtStr = detail.bankTime;
for (int ci = 0; ci < dtStr.size(); ++ci) {
const QChar ch = dtStr.at(ci);
if (ch != ' ' && ch.category() == QChar::Separator_Space) dtStr[ci] = ' ';
}
QDateTime txTime = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm");
if (!txTime.isValid()) txTime = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm");
if (txTime.isValid()) txTime.setTimeZone(msk);
// Проверяем ±1 час
if (m_searchTime.isValid() && txTime.isValid()) {
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC()));
if (diffSecs > 3600) {
qDebug() << "[FetchTransactions] Time mismatch: tx=" << dtStr
<< "diff=" << diffSecs << "sec, skipping";
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
continue;
}
}
// Нашли!
qDebug() << "[FetchTransactions] FOUND transaction:"
<< "amount=" << parsed.amount << "time=" << dtStr
<< "name=" << detail.name;
// Сохраняем
if (accountId != -1) {
TransactionInfo tx;
tx.accountId = accountId;
tx.amount = parsed.amount;
tx.bankName = m_appCode;
tx.bankTime = dtStr;
tx.status = TransactionStatus::Complete;
tx.timestamp = DateUtils::getUtcNow();
if (txTime.isValid()) tx.completeTime = txTime;
if (!detail.name.isEmpty()) tx.name = detail.name;
if (!detail.description.isEmpty()) tx.description = detail.description;
if (!detail.phone.isEmpty()) {
QString cleanPhone;
for (const QChar &ch : detail.phone)
if (ch.isDigit()) cleanPhone.append(ch);
tx.phone = cleanPhone;
tx.type = TransactionType::Phone;
}
TransactionDAO::insertTransactionIfNotExists(tx);
}
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
return; // успех — m_error пустой
}
// Скроллим вниз
const int x = device.screenWidth / 2;
const int offset = device.screenHeight / 4;
AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - offset, x, offset);
QThread::msleep(1000);
}
m_error = "Transaction not found: amount=" + QString::number(m_searchAmount, 'f', 2)
+ " time=" + m_searchTime.toString(Qt::ISODate);
qWarning() << "[FetchTransactions]" << m_error;
}
void GetLastTransactionsScript::scanRecentTransactions(const DeviceInfo &device, int accountId) {
const QTimeZone msk("Europe/Moscow");
const int navBarTop = device.screenHeight - (device.screenHeight / 10);
// Ждём появления транзакций
QList<Node> txButtons;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
txButtons = xmlScreenParser.findTransactionButtons(8);
if (!txButtons.isEmpty()) break;
qDebug() << "[GetLastTransactions] No transactions yet, attempt" << attempt + 1 << "/ 5";
QThread::msleep(1500);
}
qDebug() << "[GetLastTransactions] Found" << txButtons.size() << "transaction buttons";
// Находим accountId для сохранения транзакций
int accountId = -1;
const QList<AccountInfo> accounts = AccountDAO::getAccounts(m_deviceId, m_appCode);
if (!accounts.isEmpty()) {
accountId = accounts.first().id;
qDebug() << "[GetLastTransactions] Using accountId:" << accountId;
} else {
qWarning() << "[GetLastTransactions] No account found for" << m_deviceId << m_appCode;
}
// Для каждой транзакции: клик → берём дату из деталей → назад
QList<TransactionInfo> transactions;
for (int i = 0; i < txButtons.size(); ++i) {
const Node &btn = txButtons[i];
// Если кнопка внизу экрана (за навбаром) — скроллим до неё
const int navBarTop = device.screenHeight - (device.screenHeight / 10);
// Скроллим до кнопки если за экраном
if (btn.y() <= 0 || btn.y() >= navBarTop) {
qDebug() << "[GetLastTransactions] Transaction" << i + 1 << "is off-screen (y=" << btn.y() << "), scrolling...";
for (int s = 0; s < 5; ++s) {
const int x = device.screenWidth / 2;
const int offset = device.screenHeight / 4;
@ -217,121 +327,84 @@ void GetLastTransactionsScript::doStart() {
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
txButtons = xmlScreenParser.findTransactionButtons(8);
if (i < txButtons.size() && txButtons[i].y() > 0 && txButtons[i].y() < navBarTop) {
break;
}
}
if (i >= txButtons.size()) {
qWarning() << "[GetLastTransactions] Transaction" << i + 1 << "not found after scrolling";
break;
if (i < txButtons.size() && txButtons[i].y() > 0 && txButtons[i].y() < navBarTop) break;
}
if (i >= txButtons.size()) break;
}
const Node &visibleBtn = txButtons[i];
TransactionInfo tx = xmlScreenParser.parseTransactionButtonText(visibleBtn.text);
qDebug() << "[GetLastTransactions] Tapping transaction" << i + 1 << ":" << visibleBtn.text.left(50);
AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y());
QThread::msleep(2000);
// Ждём загрузки деталей — ищем дату в шапке
// Ждём загрузки деталей
TransactionInfo detail;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
detail = xmlScreenParser.parseTransactionDetail();
if (!detail.bankTime.isEmpty()) break;
// Если всё ещё на экране операций — тап не сработал, повторяем
if (xmlScreenParser.isTransactionListScreen()) {
qDebug() << "[GetLastTransactions] Still on transaction list, re-tapping" << i + 1;
AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y());
}
qDebug() << "[GetLastTransactions] Detail not loaded, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
tx.bankName = "ozon";
tx.bankName = m_appCode;
tx.bankTime = detail.bankTime;
tx.status = TransactionStatus::Complete;
tx.timestamp = DateUtils::getUtcNow();
if (!detail.name.isEmpty()) tx.name = detail.name;
if (!detail.phone.isEmpty()) {
// Оставляем только цифры
QString cleanPhone;
for (const QChar &ch : detail.phone) {
for (const QChar &ch : detail.phone)
if (ch.isDigit()) cleanPhone.append(ch);
}
tx.phone = cleanPhone;
tx.type = TransactionType::Phone;
}
// Чистим bankTime от unicode символов: заменяем все не-ASCII пробелы на обычные
// Парсим время
if (!tx.bankTime.isEmpty()) {
QString dtStr = tx.bankTime;
for (int ci = 0; ci < dtStr.size(); ++ci) {
const QChar ch = dtStr.at(ci);
if (ch != ' ' && ch.category() == QChar::Separator_Space) {
dtStr[ci] = ' ';
}
if (ch != ' ' && ch.category() == QChar::Separator_Space) dtStr[ci] = ' ';
}
tx.bankTime = dtStr;
const QTimeZone msk("Europe/Moscow");
QDateTime parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm");
if (!parsed.isValid()) {
parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm");
}
if (!parsed.isValid()) parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm");
if (parsed.isValid()) {
parsed.setTimeZone(msk);
tx.completeTime = parsed;
}
}
qDebug() << "[GetLastTransactions]"
<< "date:" << tx.bankTime
<< "name:" << tx.description
<< "sender:" << tx.name
<< "phone:" << tx.phone
<< "amount:" << (tx.amount >= 0 ? "+" : "") + QString::number(tx.amount, 'f', 2);
// Если задан лимит по количеству — проверяем счётчик
// Иначе фильтруем по 24 часам
if (m_maxCount > 0) {
if (transactions.size() + 1 >= m_maxCount) {
// Эта транзакция последняя — сохраним и выйдем после обработки
}
} else if (tx.completeTime.isValid()) {
// Фильтр по времени (24ч) если нет maxCount
if (m_maxCount <= 0 && tx.completeTime.isValid()) {
const qint64 ageSecs = tx.completeTime.toUTC().secsTo(QDateTime::currentDateTimeUtc());
if (ageSecs > 24 * 3600) {
qDebug() << "[GetLastTransactions] Transaction older than 24h, stopping";
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
break;
}
}
// Сохраняем в БД если не существует
// Берём первый аккаунт для этого устройства и приложения
// Сохраняем
if (accountId != -1) {
tx.accountId = accountId;
if (TransactionDAO::insertTransactionIfNotExists(tx)) {
qDebug() << "[GetLastTransactions] Saved transaction to DB";
// Делаем скриншот документа транзакции
// Скриншот чека
Node docBtn = xmlScreenParser.findButtonNode("Документы", false);
if (!docBtn.isEmpty()) {
qDebug() << "[GetLastTransactions] Tapping 'Документы'...";
AdbUtils::makeTap(m_deviceId, docBtn.x(), docBtn.y());
QThread::msleep(2000);
// Ждём появления кнопки "Поделиться"
bool docLoaded = false;
for (int da = 0; da < 20; ++da) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
Node shareBtn = xmlScreenParser.findNodeByContentDesc("Поделиться");
if (!shareBtn.isEmpty()) {
docLoaded = true;
break;
if (!xmlScreenParser.findNodeByContentDesc("Поделиться").isEmpty()) {
docLoaded = true; break;
}
qDebug() << "[GetLastTransactions] Waiting for 'Поделиться', attempt" << da + 1 << "/ 20";
QThread::msleep(1500);
}
@ -346,70 +419,42 @@ void GetLastTransactionsScript::doStart() {
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 ---";
qDebug() << "[GetLastTransactions] OCR hex:"
<< ocrText.left(200).toUtf8().toHex(' ');
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";
}
} else {
qWarning() << "[GetLastTransactions] 'Поделиться' not found, skipping receipt";
}
// Возвращаемся из документов в детали транзакции
}
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
}
} else {
qDebug() << "[GetLastTransactions] Transaction already exists or error";
}
}
transactions.append(tx);
// Если достигнут лимит по количеству — выходим
if (m_maxCount > 0 && transactions.size() >= m_maxCount) {
qDebug() << "[GetLastTransactions] Reached maxCount limit:" << m_maxCount;
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
break;
}
// Возвращаемся в список
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
// Ждём возврата на экран списка
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isTransactionListScreen()) break;
QThread::msleep(1000);
}
// Обновляем кнопки (после возврата координаты могут измениться)
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
txButtons = xmlScreenParser.findTransactionButtons(8);
}
emit finishedWithResult(m_error);
}
} // namespace Ozon

View File

@ -1,5 +1,8 @@
#pragma once
#include "CommonScript.h"
#include <QDateTime>
#include "db/DeviceInfo.h"
#include "db/BankProfileInfo.h"
namespace Ozon {
@ -11,6 +14,8 @@ public:
QString deviceId,
QString appCode,
int maxCount = 0,
double searchAmount = 0,
QDateTime searchTime = {},
QObject *parent = nullptr
);
@ -20,9 +25,20 @@ protected:
void doStart() override;
private:
const QString m_deviceId;
// Навигация до экрана "Операции", возвращает true если успешно
bool navigateToTransactionList(const DeviceInfo &device, const BankProfileInfo &app);
// Сканирование последних транзакций (для UI: 24ч или maxCount)
void scanRecentTransactions(const DeviceInfo &device, int accountId);
// Поиск конкретной транзакции по сумме и времени (для задач FETCH_TRANSACTIONS)
void searchTransaction(const DeviceInfo &device, int accountId);
QString m_deviceId;
const QString m_appCode;
const int m_maxCount; // 0 = без лимита (фильтр по 24ч), >0 = только N последних
const int m_maxCount; // 0 = фильтр по 24ч, >0 = N последних
const double m_searchAmount; // сумма в рублях (0 = не искать конкретную)
const QDateTime m_searchTime; // примерное время (UTC)
QString m_error;
};

View File

@ -1,14 +1,15 @@
#include "GetProfileInfoScript.h"
#include <QJsonDocument>
#include <QRegularExpression>
#include <QJsonObject>
#include <QMap>
#include <QtGlobal>
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "net/NetworkService.h"
#include "AppLogger.h"
@ -26,8 +27,8 @@ GetProfileInfoScript::GetProfileInfoScript(
GetProfileInfoScript::~GetProfileInfoScript() = default;
void GetProfileInfoScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
@ -53,7 +54,7 @@ void GetProfileInfoScript::doStart() {
// Если мы не на домашнем экране — перезапускаем приложение и вводим пин-код
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[GetProfileInfo] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode;
qWarning() << m_error;
@ -107,17 +108,18 @@ void GetProfileInfoScript::doStart() {
// Извлекаем полное имя и телефон
const QString fullName = xmlScreenParser.parseProfileFullName();
const QString phone = xmlScreenParser.parseProfilePhone().simplified().remove(' ');
QString phone = xmlScreenParser.parseProfilePhone();
phone.remove(QRegularExpression(R"(\s+)")); // убираем все виды пробелов (включая \u00A0)
qDebug() << "[GetProfileInfo] fullName:" << fullName << "phone:" << phone;
if (!ApplicationDAO::updateProfileInfo(app.id, fullName, phone)) {
if (!BankProfileDAO::updateProfileInfo(app.id, fullName, phone)) {
m_error = "Failed to save profile info to DB: " + m_deviceId;
qWarning() << m_error;
}
// Отправляем bank_profile на сервер
const ApplicationInfo freshApp = ApplicationDAO::getApplication(m_deviceId, m_appCode);
const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode);
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
@ -133,6 +135,7 @@ void GetProfileInfoScript::doStart() {
profileBody["phone_number"] = phone;
profileBody["balance"] = QString::number(qRound64(freshApp.amount * 100));
profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled";
profileBody["device_id"] = device.apiId;
profileBody["currency"] = currencyCode;
NetworkService ns;
@ -142,7 +145,7 @@ void GetProfileInfoScript::doStart() {
if (freshApp.bankProfileId.isEmpty()) {
ns.addExtra("app_id", freshApp.id);
ns.postBankProfile();
const ApplicationInfo afterPost = ApplicationDAO::getApplication(m_deviceId, m_appCode);
const BankProfileInfo afterPost = BankProfileDAO::getApplication(device.id, m_appCode);
if (afterPost.bankProfileId.isEmpty()) {
const QString err = "postBankProfile failed for " + m_deviceId;
qWarning() << "[GetProfileInfo]" << err;
@ -159,8 +162,8 @@ void GetProfileInfoScript::doStart() {
}
// Отправляем данные аккаунтов на сервер если нет material_id
const QList<AccountInfo> allAccounts = AccountDAO::getAccounts(m_deviceId, m_appCode);
QList<QPair<AccountInfo, Node>> accountsToPost;
const QList<MaterialInfo> allAccounts = MaterialDAO::getAccounts(device.id, m_appCode);
QList<QPair<MaterialInfo, Node>> accountsToPost;
for (const auto &acc : allAccounts) {
if (acc.materialId.isEmpty()) {
accountsToPost.append({acc, Node()});

View File

@ -19,7 +19,7 @@ protected:
void doStart() override;
private:
const QString m_deviceId;
QString m_deviceId;
const QString m_appCode;
QString m_error;
};

View File

@ -3,8 +3,8 @@
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "AppLogger.h"
#include "GetCardInfoScript.h"
@ -14,18 +14,21 @@ namespace Ozon {
LoginAndCheckAccountsScript::LoginAndCheckAccountsScript(
QString deviceId,
QString appCode,
QObject *parent
QObject *parent,
bool fetchProfileOnly
) : CommonScript(parent),
m_deviceId(std::move(deviceId)),
m_appCode(std::move(appCode)) {
m_appCode(std::move(appCode)),
m_fetchProfileOnly(fetchProfileOnly) {
}
LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default;
void LoginAndCheckAccountsScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
// m_deviceId = ADB serial, device.id = android_id (для БД)
DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial;
const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
@ -36,47 +39,46 @@ void LoginAndCheckAccountsScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) {
if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) {
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),
"FETCH_PROFILE");
if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) {
qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode;
}
BankProfileDAO::updateComment(app.id,
"Не прошла проверка " + QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm:ss"));
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
} else {
if (!ApplicationDAO::updatePinCodeStatus(app.id, "yes", "")) {
qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode;
}
// Создаём bank profile если ещё не создан
createBankProfileIfNeeded(m_deviceId, m_appCode);
createBankProfileIfNeeded(device.id, m_appCode);
if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) {
QList<QPair<AccountInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
if (goToAllProducts(m_deviceId, device.screenWidth, device.screenHeight)) {
QList<QPair<MaterialInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
for (auto &[account, node]: accounts) {
account.deviceId = m_deviceId;
account.deviceId = device.id;
account.appCode = m_appCode;
if (!AccountDAO::upsertAccount(account)) {
if (!MaterialDAO::upsertAccount(account)) {
m_error = "Not updated: " + account.lastNumbers;
qCritical() << m_error;
}
}
if (!m_fetchProfileOnly) {
postAccountsData(accounts);
}
} else {
qWarning() << "Cant go to all products: " << m_deviceId << " " << m_appCode;
}
if (!m_fetchProfileOnly) {
// Запускаем сканирование баланса, данных карт и отправку на сервер (синхронно)
GetCardInfoScript cardScript(m_deviceId, m_appCode, "");
cardScript.start();
}
}
emit finishedWithResult(m_error);
}

View File

@ -12,15 +12,17 @@ public:
explicit LoginAndCheckAccountsScript(
QString deviceId,
QString appCode,
QObject *parent = nullptr
QObject *parent = nullptr,
bool fetchProfileOnly = false
);
protected:
void doStart() override;
private:
const QString m_deviceId;
QString m_deviceId;
const QString m_appCode;
const bool m_fetchProfileOnly;
QString m_error;
};

View File

@ -10,8 +10,8 @@
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "time/DateUtils.h"
@ -59,7 +59,7 @@ static QString extractTransactionId(const QString &ocrText) {
namespace Ozon {
PayByCardScript::PayByCardScript(
AccountInfo account,
MaterialInfo account,
QString cardNumber,
const double amount,
QObject *parent
@ -72,7 +72,7 @@ PayByCardScript::~PayByCardScript() = default;
void PayByCardScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode);
const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_account.deviceId;
@ -94,11 +94,11 @@ void PayByCardScript::doStart() {
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[PayByCard] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.id;
qWarning() << m_error;
AdbUtils::tryToKillApplication(device.id, app.package);
AdbUtils::tryToKillApplication(device.adbSerial, app.package);
emit finishedWithResult(m_error);
return;
}
@ -126,7 +126,7 @@ void PayByCardScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight);
makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}

View File

@ -10,7 +10,7 @@ public:
~PayByCardScript() override;
PayByCardScript(
AccountInfo account,
MaterialInfo account,
QString cardNumber,
double amount,
QObject *parent = nullptr
@ -20,7 +20,7 @@ protected:
void doStart() override;
private:
const AccountInfo m_account;
const MaterialInfo m_account;
const QString m_cardNumber;
const double m_amount;
QString m_error;

View File

@ -10,8 +10,8 @@
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "time/DateUtils.h"
@ -59,7 +59,7 @@ static QString extractTransactionId(const QString &ocrText) {
namespace Ozon {
PayByPhoneScript::PayByPhoneScript(
AccountInfo account,
MaterialInfo account,
QString phone,
QString bankName,
const double amount,
@ -73,7 +73,7 @@ PayByPhoneScript::~PayByPhoneScript() = default;
void PayByPhoneScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode);
const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_account.deviceId;
@ -95,11 +95,11 @@ void PayByPhoneScript::doStart() {
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[PayByPhone] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.id;
qWarning() << m_error;
AdbUtils::tryToKillApplication(device.id, app.package);
AdbUtils::tryToKillApplication(device.adbSerial, app.package);
emit finishedWithResult(m_error);
return;
}
@ -127,7 +127,7 @@ void PayByPhoneScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight);
makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}

View File

@ -10,7 +10,7 @@ public:
~PayByPhoneScript() override;
PayByPhoneScript(
AccountInfo account,
MaterialInfo account,
QString phone,
QString bankName,
double amount,
@ -21,7 +21,7 @@ protected:
void doStart() override;
private:
const AccountInfo m_account;
const MaterialInfo m_account;
const QString m_phone;
const QString m_bankName;
const double m_amount;

View File

@ -13,7 +13,7 @@
#include "adb/AdbUtils.h"
#include "android/xml/Node.h"
#include "db/AccountInfo.h"
#include "db/MaterialInfo.h"
#include "db/TransactionInfo.h"
#include "time/DateUtils.h"
@ -358,7 +358,7 @@ Node ScreenXmlParser::findCardButtonByLastNumbers(const QString &lastNumbers) {
return {};
}
QList<AccountInfo> ScreenXmlParser::parseCardsOnHomeScreen() {
QList<MaterialInfo> ScreenXmlParser::parseCardsOnHomeScreen() {
// "Основной счёт X ₽" — общая кнопка с балансом для всех карт.
// Карты — отдельные кнопки с текстом из 4 цифр (могут быть любые non-breaking spaces).
// Сумма из "Основной счёт" проставляется на все карты.
@ -388,13 +388,13 @@ QList<AccountInfo> ScreenXmlParser::parseCardsOnHomeScreen() {
qWarning() << "[parseCards] Balance button not found or amount=0";
// 2. Собираем все кнопки карт (текст заканчивается на 4 цифры)
QList<AccountInfo> cards;
QList<MaterialInfo> cards;
for (const Node &node : m_nodes) {
if (node.className != "android.widget.Button" || !node.clickable) continue;
QString t = node.text.trimmed();
if (!endsWithFourDigits.match(t).hasMatch()) continue;
AccountInfo acc;
MaterialInfo acc;
acc.lastNumbers = t.right(4);
acc.amount = commonAmount;
acc.currency = "RUB";
@ -1329,8 +1329,8 @@ TransactionInfo ScreenXmlParser::parseTransactionInfo(int screenWidth, int scree
}
QList<QPair<AccountInfo, Node> > ScreenXmlParser::parseAccountsInfo() {
QList<QPair<AccountInfo, Node> > accounts;
QList<QPair<MaterialInfo, Node> > ScreenXmlParser::parseAccountsInfo() {
QList<QPair<MaterialInfo, Node> > accounts;
for (const Node &node: m_nodes) {
const QString text = node.text;
@ -1343,7 +1343,7 @@ QList<QPair<AccountInfo, Node> > ScreenXmlParser::parseAccountsInfo() {
continue;
}
AccountInfo info;
MaterialInfo info;
info.lastNumbers = match.captured(1).remove(" ");
info.amount = match.captured(2).remove(" ").toFloat();
info.description = text.mid(0, text.indexOf("Номер карты"));
@ -1495,7 +1495,7 @@ void ScreenXmlParser::test() {
// Номер карты с последними цифрами
QDomElement root = doc.documentElement();
parseAndSaveXml(doc.toString());
// for (const AccountInfo &info: parseAccountsInfo()) {
// for (const MaterialInfo &info: parseAccountsInfo()) {
// qDebug().noquote().nospace() << convertAccountToString(info);
// }
// TransactionInfo info = parseExpandedInfo(root);

View File

@ -3,7 +3,7 @@
#include <QObject>
#include <QDomElement>
#include "android/xml/Node.h"
#include "db/AccountInfo.h"
#include "db/MaterialInfo.h"
#include "db/TransactionInfo.h"
namespace Ozon {
@ -50,9 +50,9 @@ public:
// Finds a card button whose text ends with the given last 4 digits
Node findCardButtonByLastNumbers(const QString &lastNumbers);
// Returns all cards visible on the home screen as AccountInfo stubs
// Returns all cards visible on the home screen as MaterialInfo stubs
// (lastNumbers, description, amount filled; id=-1, cardNumber empty)
QList<AccountInfo> parseCardsOnHomeScreen();
QList<MaterialInfo> parseCardsOnHomeScreen();
bool isProfileScreen();
@ -76,7 +76,7 @@ public:
QList<TransactionInfo> parseTodayAndYesterdayHistory();
QList<QPair<AccountInfo, Node> > parseAccountsInfo();
QList<QPair<MaterialInfo, Node> > parseAccountsInfo();
// Finds a TextView with full card number (format "XXXX XXXX XXXX XXXX")
QString findFullCardNumber();

View File

@ -81,34 +81,16 @@ QString DatabaseManager::connectionName() {
}
static bool columnExists(QSqlQuery &q, const QString &table, const QString &column) {
// Валидация: допускаем только [a-zA-Z0-9_] чтобы исключить SQL injection
static const QRegularExpression valid("^[a-zA-Z_][a-zA-Z0-9_]*$");
if (!valid.match(table).hasMatch() || !valid.match(column).hasMatch()) {
qCritical() << "[DB] columnExists: invalid identifier" << table << column;
return false;
}
q.exec(QString("SELECT COUNT(*) FROM pragma_table_info('%1') WHERE name='%2'")
.arg(table, column));
return q.next() && q.value(0).toInt() > 0;
}
void DatabaseManager::initSchema() {
QSqlDatabase db = database();
QSqlQuery q(db);
// Читаем текущую версию схемы
q.exec("PRAGMA user_version");
const int version = q.next() ? q.value(0).toInt() : 0;
// ── Версия 1: полная схема ─────────────────────────────────────────────────
if (version < 1) {
db.transaction();
q.exec(R"(CREATE TABLE IF NOT EXISTS devices (
id TEXT PRIMARY KEY,
api_id TEXT,
adb_serial TEXT,
status TEXT,
name TEXT,
android TEXT,
@ -120,7 +102,7 @@ void DatabaseManager::initSchema() {
image BLOB
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS applications (
q.exec(R"(CREATE TABLE IF NOT EXISTS bank_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
pin_code TEXT,
@ -141,7 +123,7 @@ void DatabaseManager::initSchema() {
UNIQUE (device_id, code)
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS accounts (
q.exec(R"(CREATE TABLE IF NOT EXISTS materials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
app_code TEXT,
@ -178,7 +160,7 @@ void DatabaseManager::initSchema() {
complete_time DATETIME,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
receipt_image BLOB,
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS events (
@ -197,7 +179,8 @@ void DatabaseManager::initSchema() {
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
json TEXT DEFAULT '',
sent_to_server INTEGER DEFAULT 0,
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
screenshot BLOB,
FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE
))");
@ -219,28 +202,6 @@ void DatabaseManager::initSchema() {
q.exec("CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC)");
q.exec(R"(CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT
))");
db.commit();
q.exec("PRAGMA user_version = 1");
qDebug() << "[DB] Migration 1 applied";
}
// ── Версия 2: добавляем email в applications ─────────────────────────────
if (version < 2) {
db.transaction();
q.exec("ALTER TABLE applications ADD COLUMN email TEXT DEFAULT ''");
db.commit();
q.exec("PRAGMA user_version = 2");
qDebug() << "[DB] Migration 2 applied";
}
// ── Версия 3: таблица general_logs для всех Qt-логов ────────────────────
if (version < 3) {
db.transaction();
q.exec(R"(CREATE TABLE IF NOT EXISTS general_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
level TEXT NOT NULL DEFAULT '',
@ -248,11 +209,15 @@ void DatabaseManager::initSchema() {
message TEXT NOT NULL,
timestamp TEXT NOT NULL
))");
q.exec("CREATE INDEX IF NOT EXISTS idx_general_logs_id ON general_logs (id DESC)");
q.exec(R"(CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT
))");
db.commit();
q.exec("PRAGMA user_version = 3");
qDebug() << "[DB] Migration 3 applied";
}
}
void DatabaseManager::closeConnection() {

View File

@ -68,3 +68,30 @@ QByteArray AppLogDAO::getScreenshot(int id) {
if (!q.exec() || !q.next()) return {};
return q.value(0).toByteArray();
}
QList<AppLogEntry> AppLogDAO::getLogsWithScreenshots(int limit) {
QList<AppLogEntry> result;
QSqlQuery q(DatabaseManager::instance().database());
q.prepare(R"(
SELECT id, source, device_id, message, screenshot, timestamp
FROM app_logs
ORDER BY id DESC
LIMIT :limit
)");
q.bindValue(":limit", limit);
if (!q.exec()) {
qWarning() << "[AppLogDAO] getLogsWithScreenshots failed:" << q.lastError().text();
return result;
}
while (q.next()) {
AppLogEntry e;
e.id = q.value("id").toInt();
e.source = q.value("source").toString();
e.deviceId = q.value("device_id").toString();
e.message = q.value("message").toString();
e.screenshot = q.value("screenshot").toByteArray();
e.timestamp = DateUtils::fromUtcString(q.value("timestamp").toString());
result.append(e);
}
return result;
}

View File

@ -14,4 +14,7 @@ public:
// Загружает скриншот отдельно по id (по клику в UI)
[[nodiscard]] static QByteArray getScreenshot(int id);
// Все записи со скриншотами (для экспорта)
[[nodiscard]] static QList<AppLogEntry> getLogsWithScreenshots(int limit = 100);
};

View File

@ -2,15 +2,15 @@
#include <QSqlError>
#include <QVariant>
#include <QSettings>
#include "ApplicationDAO.h"
#include "BankProfileDAO.h"
#include "DatabaseManager.h"
#include "db/ApplicationInfo.h"
#include "db/BankProfileInfo.h"
bool ApplicationDAO::upsertApplication(const ApplicationInfo &info) {
bool BankProfileDAO::upsertApplication(const BankProfileInfo &info) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
INSERT INTO applications (id, device_id, pin_code, name, code, package, status, comment, install, pin_code_checked)
INSERT INTO bank_profiles (id, device_id, pin_code, name, code, package, status, comment, install, pin_code_checked)
VALUES (:id, :device_id, :pin_code, :name, :code, :package, :status, :comment, :install, :pin_code_checked)
ON CONFLICT(device_id, code)
DO UPDATE SET
@ -44,11 +44,11 @@ bool ApplicationDAO::upsertApplication(const ApplicationInfo &info) {
return true;
}
bool ApplicationDAO::updatePinCode(const QString &appId, const QString &pinCode) {
bool BankProfileDAO::updatePinCode(const QString &appId, const QString &pinCode) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE applications
UPDATE bank_profiles
SET pin_code = :pin_code
WHERE id = :id
)");
@ -63,7 +63,7 @@ bool ApplicationDAO::updatePinCode(const QString &appId, const QString &pinCode)
return true;
}
bool ApplicationDAO::update(
bool BankProfileDAO::update(
const int &appId,
const QString &pinCode,
const QString &comment,
@ -72,7 +72,7 @@ bool ApplicationDAO::update(
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE applications
UPDATE bank_profiles
SET pin_code = :pin_code, comment = :comment, status = :status
WHERE id = :id
)");
@ -89,7 +89,7 @@ bool ApplicationDAO::update(
return true;
}
bool ApplicationDAO::updatePinCodeStatus(
bool BankProfileDAO::updatePinCodeStatus(
const int &appId,
const QString &pinCodeStatus,
const QString &comment
@ -97,15 +97,14 @@ bool ApplicationDAO::updatePinCodeStatus(
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE applications
SET pin_code_checked = :pin_code_checked, comment = :comment, status = :status
UPDATE bank_profiles
SET pin_code_checked = :pin_code_checked, comment = :comment
WHERE id = :id
)");
query.bindValue(":id", appId);
query.bindValue(":comment", comment);
query.bindValue(":pin_code_checked", pinCodeStatus);
query.bindValue(":status", pinCodeStatus == "yes" ? "active" : "off");
if (!query.exec()) {
qWarning() << "Ошибка при обновлении PIN-кода приложения:" << query.lastError().text();
@ -115,8 +114,20 @@ bool ApplicationDAO::updatePinCodeStatus(
}
ApplicationInfo parseApplication(const QSqlQuery &query) {
ApplicationInfo app;
bool BankProfileDAO::updateComment(const int appId, const QString &comment) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE bank_profiles SET comment = :comment WHERE id = :id");
query.bindValue(":comment", comment);
query.bindValue(":id", appId);
if (!query.exec()) {
qWarning() << "Ошибка при обновлении комментария:" << query.lastError().text();
return false;
}
return true;
}
BankProfileInfo parseApplication(const QSqlQuery &query) {
BankProfileInfo app;
app.id = query.value("id").toInt();
app.deviceId = query.value("device_id").toString();
app.pinCode = query.value("pin_code").toString();
@ -136,12 +147,12 @@ ApplicationInfo parseApplication(const QSqlQuery &query) {
return app;
}
ApplicationInfo ApplicationDAO::getApplication(const QString &deviceId, const QString &appCode) {
BankProfileInfo BankProfileDAO::getApplication(const QString &deviceId, const QString &appCode) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount
FROM applications
FROM bank_profiles
WHERE device_id = :device_id AND code = :code
LIMIT 1
)");
@ -160,13 +171,13 @@ ApplicationInfo ApplicationDAO::getApplication(const QString &deviceId, const QS
return {};
}
QList<ApplicationInfo> ApplicationDAO::getApplicationsByDeviceId(const QString &deviceId) {
QList<BankProfileInfo> BankProfileDAO::getApplicationsByDeviceId(const QString &deviceId) {
QSqlQuery query(DatabaseManager::instance().database());
QList<ApplicationInfo> list;
QList<BankProfileInfo> list;
query.prepare(R"(
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount
FROM applications
FROM bank_profiles
WHERE device_id = :device_id AND install = 'yes'
)");
query.bindValue(":device_id", deviceId);
@ -183,7 +194,30 @@ QList<ApplicationInfo> ApplicationDAO::getApplicationsByDeviceId(const QString &
return list;
}
bool ApplicationDAO::updateProfileInfo(
QList<BankProfileInfo> BankProfileDAO::getAllApplicationsByDeviceId(const QString &deviceId) {
QSqlQuery query(DatabaseManager::instance().database());
QList<BankProfileInfo> list;
query.prepare(R"(
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount
FROM bank_profiles
WHERE device_id = :device_id
)");
query.bindValue(":device_id", deviceId);
if (!query.exec()) {
qWarning() << "Ошибка при получении всех приложений по deviceId:" << query.lastError().text();
return {};
}
while (query.next()) {
list.append(parseApplication(query));
}
return list;
}
bool BankProfileDAO::updateProfileInfo(
const int &appId,
const QString &fullName,
const QString &phone,
@ -192,7 +226,7 @@ bool ApplicationDAO::updateProfileInfo(
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE applications
UPDATE bank_profiles
SET full_name = :full_name, phone = :phone, email = :email
WHERE id = :id
)");
@ -209,9 +243,9 @@ bool ApplicationDAO::updateProfileInfo(
return true;
}
bool ApplicationDAO::updateAmount(const int appId, const double amount) {
bool BankProfileDAO::updateAmount(const int appId, const double amount) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE applications SET amount = :amount WHERE id = :id");
query.prepare("UPDATE bank_profiles SET amount = :amount WHERE id = :id");
query.bindValue(":amount", amount);
query.bindValue(":id", appId);
if (!query.exec()) {
@ -221,9 +255,9 @@ bool ApplicationDAO::updateAmount(const int appId, const double amount) {
return true;
}
bool ApplicationDAO::updateBankProfileId(const int appId, const QString &bankProfileId) {
bool BankProfileDAO::updateBankProfileId(const int appId, const QString &bankProfileId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE applications SET bank_profile_id = :bank_profile_id WHERE id = :id");
query.prepare("UPDATE bank_profiles SET bank_profile_id = :bank_profile_id WHERE id = :id");
query.bindValue(":bank_profile_id", bankProfileId);
query.bindValue(":id", appId);
if (!query.exec()) {
@ -233,17 +267,17 @@ bool ApplicationDAO::updateBankProfileId(const int appId, const QString &bankPro
return true;
}
QList<ApplicationInfo> ApplicationDAO::findAllByBankProfileId(const QString &bankProfileId) {
QList<ApplicationInfo> list;
QList<BankProfileInfo> BankProfileDAO::findAllByBankProfileId(const QString &bankProfileId) {
QList<BankProfileInfo> list;
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount
FROM applications
FROM bank_profiles
WHERE bank_profile_id = :bank_profile_id
)");
query.bindValue(":bank_profile_id", bankProfileId);
if (!query.exec()) {
qWarning() << "Ошибка при поиске applications по bank_profile_id:" << query.lastError().text();
qWarning() << "Ошибка при поиске bank_profiles по bank_profile_id:" << query.lastError().text();
return list;
}
while (query.next()) {
@ -252,10 +286,10 @@ QList<ApplicationInfo> ApplicationDAO::findAllByBankProfileId(const QString &ban
return list;
}
bool ApplicationDAO::activateAppsForDevice(const QString &deviceId) {
bool BankProfileDAO::activateAppsForDevice(const QString &deviceId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE applications SET status = 'active'
UPDATE bank_profiles SET status = 'active'
WHERE device_id = :device_id AND install = 'yes' AND pin_code_checked = 'yes'
)");
query.bindValue(":device_id", deviceId);
@ -266,10 +300,10 @@ bool ApplicationDAO::activateAppsForDevice(const QString &deviceId) {
return true;
}
bool ApplicationDAO::deactivateAppsForDevice(const QString &deviceId) {
bool BankProfileDAO::deactivateAppsForDevice(const QString &deviceId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE applications SET status = 'off'
UPDATE bank_profiles SET status = 'off'
WHERE device_id = :device_id AND install = 'yes'
)");
query.bindValue(":device_id", deviceId);
@ -280,7 +314,7 @@ bool ApplicationDAO::deactivateAppsForDevice(const QString &deviceId) {
return true;
}
bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet<QString> &apps) {
bool BankProfileDAO::checkInstalledApps(const QString &deviceId, const QSet<QString> &apps) {
QSqlQuery query(DatabaseManager::instance().database());
const QSettings settings("config.ini", QSettings::IniFormat);
QStringList banks = settings.value("apps/list", QStringList()).toStringList();
@ -290,7 +324,7 @@ bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet<QStr
QStringList placeholders;
for (int i = 0; i < banks.size(); ++i) placeholders << "?";
query.prepare(QString(R"(
UPDATE applications SET install = 'no'
UPDATE bank_profiles SET install = 'no'
WHERE device_id = ? AND code NOT IN (%1)
)").arg(placeholders.join(",")));
query.addBindValue(deviceId);
@ -307,7 +341,7 @@ bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet<QStr
const QString currency = settings.value(bank + "/currency").toString();
query.prepare(R"(
INSERT INTO applications (device_id, name, code, package, status, install, pin_code_checked, currency)
INSERT INTO bank_profiles (device_id, name, code, package, status, install, pin_code_checked, currency)
VALUES (:device_id, :name, :code, :package, :status, :install, :pin_code_checked, :currency)
ON CONFLICT(device_id, code)
DO UPDATE SET

View File

@ -1,10 +1,10 @@
#pragma once
#include "db/ApplicationInfo.h"
#include "db/BankProfileInfo.h"
class ApplicationDAO {
class BankProfileDAO {
public:
[[nodiscard]] static bool upsertApplication(const ApplicationInfo &info);
[[nodiscard]] static bool upsertApplication(const BankProfileInfo &info);
[[nodiscard]] static bool updatePinCode(const QString &appId, const QString &pinCode);
@ -21,6 +21,8 @@ public:
const QString &comment
);
[[nodiscard]] static bool updateComment(int appId, const QString &comment);
[[nodiscard]] static bool updateProfileInfo(
const int &appId,
const QString &fullName,
@ -30,13 +32,15 @@ public:
[[nodiscard]] static bool updateBankProfileId(int appId, const QString &bankProfileId);
[[nodiscard]] static QList<ApplicationInfo> findAllByBankProfileId(const QString &bankProfileId);
[[nodiscard]] static QList<BankProfileInfo> findAllByBankProfileId(const QString &bankProfileId);
[[nodiscard]] static bool updateAmount(int appId, double amount);
[[nodiscard]] static ApplicationInfo getApplication(const QString &deviceId, const QString &appCode);
[[nodiscard]] static BankProfileInfo getApplication(const QString &deviceId, const QString &appCode);
[[nodiscard]] static QList<ApplicationInfo> getApplicationsByDeviceId(const QString &deviceId);
[[nodiscard]] static QList<BankProfileInfo> getApplicationsByDeviceId(const QString &deviceId);
[[nodiscard]] static QList<BankProfileInfo> getAllApplicationsByDeviceId(const QString &deviceId);
[[nodiscard]] static bool checkInstalledApps(const QString &deviceId, const QSet<QString> &apps);

View File

@ -9,6 +9,7 @@ DeviceInfo parseDevice(const QSqlQuery &query) {
DeviceInfo device;
device.id = query.value("id").toString();
device.apiId = query.value("api_id").toString();
device.adbSerial = query.value("adb_serial").toString();
device.status = query.value("status").toString();
device.name = query.value("name").toString();
device.android = query.value("android").toString();
@ -25,7 +26,7 @@ QList<DeviceInfo> DeviceDAO::getAllDevices(const bool online) {
QSqlQuery query(DatabaseManager::instance().database());
QString sql = R"(
SELECT id, api_id, status, name, android, screen_width, screen_height, battery, update_time, image
SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image
FROM devices
)";
if (online) {
@ -71,7 +72,7 @@ DeviceInfo DeviceDAO::getDeviceById(const QString &deviceId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, api_id, status, name, android, screen_width, screen_height, battery, update_time, image
SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image
FROM devices
WHERE id = :deviceId
)");
@ -89,6 +90,69 @@ DeviceInfo DeviceDAO::getDeviceById(const QString &deviceId) {
return {};
}
DeviceInfo DeviceDAO::findByAdbSerial(const QString &adbSerial) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image
FROM devices
WHERE adb_serial = :adb_serial
LIMIT 1
)");
query.bindValue(":adb_serial", adbSerial);
if (!query.exec()) {
qWarning() << "Ошибка при поиске устройства по adb_serial:" << query.lastError().text();
return {};
}
if (query.next()) {
return parseDevice(query);
}
return {};
}
DeviceInfo DeviceDAO::findByName(const QString &name) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image
FROM devices
WHERE name = :name
LIMIT 1
)");
query.bindValue(":name", name);
if (!query.exec()) {
qWarning() << "Ошибка при поиске устройства по name:" << query.lastError().text();
return {};
}
if (query.next()) {
return parseDevice(query);
}
return {};
}
DeviceInfo DeviceDAO::findUnmatchedDevice() {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image
FROM devices
WHERE api_id IS NOT NULL AND api_id != ''
AND (adb_serial IS NULL OR adb_serial = '')
LIMIT 1
)");
if (!query.exec()) {
qWarning() << "Ошибка при поиске unmatched device:" << query.lastError().text();
return {};
}
if (query.next()) {
return parseDevice(query);
}
return {};
}
bool DeviceDAO::upsertDevice(const DeviceInfo &device) {
QSqlQuery query(DatabaseManager::instance().database());
@ -107,6 +171,8 @@ bool DeviceDAO::upsertDevice(const DeviceInfo &device) {
// Обновление
query.prepare(R"(
UPDATE devices SET
api_id = CASE WHEN :api_id != '' THEN :api_id ELSE api_id END,
adb_serial = CASE WHEN :adb_serial != '' THEN :adb_serial ELSE adb_serial END,
status = :status,
name = :name,
android = :android,
@ -120,14 +186,16 @@ bool DeviceDAO::upsertDevice(const DeviceInfo &device) {
// Вставка
query.prepare(R"(
INSERT INTO devices (
id, status, name, android, screen_width, screen_height, battery, update_time
id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time
) VALUES (
:id, :status, :name, :android, :width, :height, :battery, :update_time
:id, :api_id, :adb_serial, :status, :name, :android, :width, :height, :battery, :update_time
)
)");
}
query.bindValue(":id", device.id);
query.bindValue(":api_id", device.apiId);
query.bindValue(":adb_serial", device.adbSerial);
query.bindValue(":status", device.status);
query.bindValue(":name", device.name);
query.bindValue(":android", device.android);

View File

@ -6,6 +6,12 @@ class DeviceDAO {
public:
[[nodiscard]] static DeviceInfo getDeviceById(const QString &deviceId);
[[nodiscard]] static DeviceInfo findByAdbSerial(const QString &adbSerial);
[[nodiscard]] static DeviceInfo findByName(const QString &name);
[[nodiscard]] static DeviceInfo findUnmatchedDevice();
[[nodiscard]] static bool upsertDevice(const DeviceInfo &device);
[[nodiscard]] static bool updateApiId(const QString &deviceId, const QString &apiId);

View File

@ -160,7 +160,14 @@ EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) {
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
FROM events
WHERE status = :status AND device_id = :device_id
ORDER BY timestamp ASC
ORDER BY
CASE type
WHEN 'CREATE_TRANSACTION' THEN 0
WHEN 'FETCH_TRANSACTIONS' THEN 1
WHEN 'FETCH_PROFILE' THEN 2
ELSE 3
END ASC,
timestamp ASC
LIMIT 1
)");
query.bindValue(":status", eventStatusToString(EventStatus::Wait));
@ -258,3 +265,78 @@ bool EventDAO::existsActiveByExternalId(const QString &externalId) {
}
return query.next();
}
bool EventDAO::cancelWaitingByType(const QString &deviceId, EventType type) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE events SET status = :cancelled, comment = 'Cancelled: replaced by new task'
WHERE device_id = :device_id AND type = :type AND status = :wait
)");
query.bindValue(":cancelled", eventStatusToString(EventStatus::Error));
query.bindValue(":device_id", deviceId);
query.bindValue(":type", eventTypeToString(type));
query.bindValue(":wait", eventStatusToString(EventStatus::Wait));
if (!query.exec()) {
qWarning() << "Ошибка при cancelWaitingByType:" << query.lastError().text();
return false;
}
return true;
}
QList<EventInfo> EventDAO::cancelWaitingByBank(const QString &deviceId, const QString &bankName) {
// Сначала получаем задачи которые будем отменять (для отправки на сервер)
QList<EventInfo> cancelled;
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, account_id, device_id, external_id, amount, phone, bank_name,
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
FROM events
WHERE device_id = :device_id AND bank_name = :bank_name AND status = :wait
)");
query.bindValue(":device_id", deviceId);
query.bindValue(":bank_name", bankName);
query.bindValue(":wait", eventStatusToString(EventStatus::Wait));
if (query.exec()) {
while (query.next()) {
cancelled.append(parseEvent(query));
}
}
// Отменяем
query.prepare(R"(
UPDATE events SET status = :error, comment = 'Bank profile disabled'
WHERE device_id = :device_id AND bank_name = :bank_name AND status = :wait
)");
query.bindValue(":error", eventStatusToString(EventStatus::Error));
query.bindValue(":device_id", deviceId);
query.bindValue(":bank_name", bankName);
query.bindValue(":wait", eventStatusToString(EventStatus::Wait));
if (!query.exec()) {
qWarning() << "Ошибка при cancelWaitingByBank:" << query.lastError().text();
}
return cancelled;
}
bool EventDAO::updateScreenshot(int id, const QByteArray &screenshot) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE events SET screenshot = :screenshot WHERE id = :id");
query.bindValue(":screenshot", screenshot.isEmpty()
? QVariant(QMetaType(QMetaType::QByteArray))
: QVariant(screenshot));
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Ошибка при updateScreenshot:" << query.lastError().text();
return false;
}
return true;
}
QByteArray EventDAO::getScreenshot(int id) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("SELECT screenshot FROM events WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec() || !query.next()) return {};
return query.value(0).toByteArray();
}

View File

@ -19,6 +19,14 @@ public:
[[nodiscard]] static bool existsActiveByExternalId(const QString &externalId);
[[nodiscard]] static bool cancelWaitingByType(const QString &deviceId, EventType type);
[[nodiscard]] static QList<EventInfo> cancelWaitingByBank(const QString &deviceId, const QString &bankName);
[[nodiscard]] static bool updateScreenshot(int id, const QByteArray &screenshot);
[[nodiscard]] static QByteArray getScreenshot(int id);
[[nodiscard]] static bool updateEventAmount(int id, double amount);
[[nodiscard]] static bool markSentToServer(int id);

View File

@ -1,16 +1,16 @@
#include <QSqlQuery>
#include <QSqlError>
#include "AccountDAO.h"
#include "MaterialDAO.h"
#include "db/AccountInfo.h"
#include "db/MaterialInfo.h"
#include "DatabaseManager.h"
#include "time/DateUtils.h"
bool AccountDAO::upsertAccount(const AccountInfo &info) {
bool MaterialDAO::upsertAccount(const MaterialInfo &info) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
INSERT INTO accounts (device_id, app_code, last_numbers, card_number, amount, update_time, status, description, currency)
INSERT INTO materials (device_id, app_code, last_numbers, card_number, amount, update_time, status, description, currency)
VALUES (:device_id, :app_code, :last_numbers, :card_number, :amount, :update_time, :status, :description, :currency)
ON CONFLICT(device_id, app_code, last_numbers)
DO UPDATE SET
@ -31,7 +31,7 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) {
query.bindValue(":description", info.description);
query.bindValue(":amount", info.amount);
query.bindValue(":update_time", DateUtils::toUtcString(info.updateTime));
query.bindValue(":status", accountStatusToString(info.status));
query.bindValue(":status", materialStatusToString(info.status));
if (!query.exec()) {
qWarning() << "Ошибка при вставке/обновлении account:" << query.lastError().text();
@ -41,7 +41,7 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) {
return true;
}
bool AccountDAO::updateAccountById(
bool MaterialDAO::updateAccountById(
const int id,
const QString &status,
const QString &description,
@ -50,7 +50,7 @@ bool AccountDAO::updateAccountById(
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE accounts
UPDATE materials
SET status = :status,
description = :description,
last_numbers = :last_numbers,
@ -72,17 +72,17 @@ bool AccountDAO::updateAccountById(
return true;
}
bool AccountDAO::deleteAccount(const int id) {
bool MaterialDAO::deleteAccount(const int id) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("DELETE FROM accounts WHERE id = :id");
query.prepare("DELETE FROM materials WHERE id = :id");
query.bindValue(":id", id);
return query.exec();
}
bool AccountDAO::updateCardNumber(const int accountId, const QString &cardNumber) {
bool MaterialDAO::updateCardNumber(const int accountId, const QString &cardNumber) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE accounts SET card_number = :card_number WHERE id = :id
UPDATE materials SET card_number = :card_number WHERE id = :id
)");
query.bindValue(":card_number", cardNumber);
query.bindValue(":id", accountId);
@ -93,9 +93,9 @@ bool AccountDAO::updateCardNumber(const int accountId, const QString &cardNumber
return true;
}
bool AccountDAO::updateMaterialId(const int accountId, const QString &materialId) {
bool MaterialDAO::updateMaterialId(const int accountId, const QString &materialId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE accounts SET material_id = :material_id WHERE id = :id");
query.prepare("UPDATE materials SET material_id = :material_id WHERE id = :id");
query.bindValue(":material_id", materialId);
query.bindValue(":id", accountId);
if (!query.exec()) {
@ -105,8 +105,8 @@ bool AccountDAO::updateMaterialId(const int accountId, const QString &materialId
return true;
}
AccountInfo parseAccount(const QSqlQuery &query) {
AccountInfo acc;
MaterialInfo parseAccount(const QSqlQuery &query) {
MaterialInfo acc;
acc.id = query.value("id").toInt();
acc.deviceId = query.value("device_id").toString();
acc.appCode = query.value("app_code").toString();
@ -115,19 +115,19 @@ AccountInfo parseAccount(const QSqlQuery &query) {
acc.materialId = query.value("material_id").toString();
acc.amount = query.value("amount").toDouble();
acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());
acc.status = accountStatusFromString(query.value("status").toString());
acc.status = materialStatusFromString(query.value("status").toString());
acc.description = query.value("description").toString();
acc.currency = query.value("currency").toString();
return acc;
}
QList<AccountInfo> AccountDAO::getAccounts(const QString &deviceId, const QString &appName) {
QList<AccountInfo> accounts;
QList<MaterialInfo> MaterialDAO::getAccounts(const QString &deviceId, const QString &appName) {
QList<MaterialInfo> accounts;
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency
FROM accounts
FROM materials
WHERE device_id = :device_id AND app_code = :app_code
)");
@ -135,7 +135,7 @@ QList<AccountInfo> AccountDAO::getAccounts(const QString &deviceId, const QStrin
query.bindValue(":app_code", appName);
if (!query.exec()) {
qWarning() << "Ошибка при получении accounts:" << query.lastError().text();
qWarning() << "Ошибка при получении materials:" << query.lastError().text();
return accounts;
}
@ -146,12 +146,12 @@ QList<AccountInfo> AccountDAO::getAccounts(const QString &deviceId, const QStrin
return accounts;
}
AccountInfo AccountDAO::getAccountById(const int accountId) {
MaterialInfo MaterialDAO::getAccountById(const int accountId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency
FROM accounts
FROM materials
WHERE id = :id
LIMIT 1
)");
@ -169,20 +169,20 @@ AccountInfo AccountDAO::getAccountById(const int accountId) {
return {};
}
QList<AccountInfo> AccountDAO::getAccountsByAppCode(const QString &appCode) {
QList<AccountInfo> accounts;
QList<MaterialInfo> MaterialDAO::getAccountsByAppCode(const QString &appCode) {
QList<MaterialInfo> accounts;
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency
FROM accounts
FROM materials
WHERE app_code = :app_code
)");
query.bindValue(":app_code", appCode);
if (!query.exec()) {
qWarning() << "Ошибка при получении accounts по app_code:" << query.lastError().text();
qWarning() << "Ошибка при получении materials по app_code:" << query.lastError().text();
return accounts;
}
@ -193,12 +193,12 @@ QList<AccountInfo> AccountDAO::getAccountsByAppCode(const QString &appCode) {
return accounts;
}
AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &appName, const QString &lastNumber) {
MaterialInfo MaterialDAO::findAppAccount(const QString &deviceId, const QString &appName, const QString &lastNumber) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency
FROM accounts
FROM materials
WHERE app_code = :appName AND last_numbers = :lastNumber AND device_id = :deviceId
LIMIT 1
)");
@ -219,13 +219,13 @@ AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &a
return {};
}
QStringList AccountDAO::getActiveMaterialIds() {
QStringList MaterialDAO::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
FROM materials a
JOIN bank_profiles app ON a.device_id = app.device_id AND a.app_code = app.code
JOIN devices d ON a.device_id = d.id
WHERE app.status = 'active'
AND a.material_id IS NOT NULL AND a.material_id != ''
@ -243,13 +243,13 @@ QStringList AccountDAO::getActiveMaterialIds() {
return ids;
}
QStringList AccountDAO::getActiveMaterialIdsExcludingDevices(const QSet<QString> &excludeDevices) {
QStringList MaterialDAO::getActiveMaterialIdsExcludingDevices(const QSet<QString> &excludeDevices) {
const QStringList all = getActiveMaterialIds();
if (excludeDevices.isEmpty()) return all;
QStringList filtered;
for (const QString &mid : all) {
const AccountInfo acc = findByMaterialId(mid);
const MaterialInfo acc = findByMaterialId(mid);
if (!excludeDevices.contains(acc.deviceId)) {
filtered << mid;
}
@ -257,12 +257,12 @@ QStringList AccountDAO::getActiveMaterialIdsExcludingDevices(const QSet<QString>
return filtered;
}
AccountInfo AccountDAO::findByMaterialId(const QString &materialId) {
MaterialInfo MaterialDAO::findByMaterialId(const QString &materialId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency
FROM accounts
FROM materials
WHERE material_id = :material_id
LIMIT 1
)");

View File

@ -1,10 +1,10 @@
#pragma once
#include <QSet>
#include "db/AccountInfo.h"
#include "db/MaterialInfo.h"
class AccountDAO {
class MaterialDAO {
public:
[[nodiscard]] static bool upsertAccount(const AccountInfo &info);
[[nodiscard]] static bool upsertAccount(const MaterialInfo &info);
[[nodiscard]] static bool updateAccountById(
int id,
@ -19,19 +19,19 @@ public:
[[nodiscard]] static bool updateMaterialId(int accountId, const QString &materialId);
[[nodiscard]] static QList<AccountInfo> getAccounts(const QString &deviceId, const QString &appName);
[[nodiscard]] static QList<MaterialInfo> getAccounts(const QString &deviceId, const QString &appName);
[[nodiscard]] static AccountInfo getAccountById(int accountId);
[[nodiscard]] static MaterialInfo getAccountById(int accountId);
[[nodiscard]] static QList<AccountInfo> getAccountsByAppCode(const QString &appCode);
[[nodiscard]] static QList<MaterialInfo> getAccountsByAppCode(const QString &appCode);
[[nodiscard]] static AccountInfo findAppAccount(
[[nodiscard]] static MaterialInfo findAppAccount(
const QString &deviceId,
const QString &appName,
const QString &lastNumber
);
[[nodiscard]] static AccountInfo findByMaterialId(const QString &materialId);
[[nodiscard]] static MaterialInfo findByMaterialId(const QString &materialId);
[[nodiscard]] static QStringList getActiveMaterialIds();

1
docs/api/openapi.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
#include <QApplication>
#include <QThread>
#include "dao/AccountDAO.h"
#include "dao/MaterialDAO.h"
#include "views/MainWindow.h"
#include "database/DatabaseManager.h"
@ -27,6 +27,7 @@
#include "black/PayByCardScript.h"
#include "ozon/PayByPhoneScript.h"
#include "widget/loader/ErrorWindow.h"
#include "widget/loader/LoaderDialog.h"
#include "widget/loader/LoaderWindow.h"
#include "widget/loader/RegistrationWindow.h"
#include <QMessageBox>
@ -79,12 +80,12 @@ void startEventHandler(const QCoreApplication &app) {
void startPayByPhoneTest() {
// Берём первый девайс с ozon аккаунтом
QList<AccountInfo> accounts = AccountDAO::getAccountsByAppCode("ozon");
QList<MaterialInfo> accounts = MaterialDAO::getAccountsByAppCode("ozon");
if (accounts.isEmpty()) {
qWarning() << "[PayByPhoneTest] No ozon accounts found";
return;
}
AccountInfo account = accounts.first();
MaterialInfo account = accounts.first();
qDebug() << "[PayByPhoneTest] Using account:" << account.id << account.deviceId << account.lastNumbers;
auto *thread = new QThread(nullptr);
@ -107,12 +108,12 @@ void startPayByPhoneTest() {
}
void startBlackPayByCardTest() {
QList<AccountInfo> accounts = AccountDAO::getAccountsByAppCode("black");
QList<MaterialInfo> accounts = MaterialDAO::getAccountsByAppCode("black");
if (accounts.isEmpty()) {
qWarning() << "[BlackPayByCardTest] No black accounts found";
return;
}
AccountInfo account = accounts.first();
MaterialInfo account = accounts.first();
qDebug() << "[BlackPayByCardTest] Using account:" << account.id << account.deviceId << account.lastNumbers;
auto *thread = new QThread(nullptr);
@ -143,7 +144,22 @@ void startNetworkService(const QCoreApplication &app) {
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater);
QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() {
auto *loader = new LoaderDialog();
loader->setWindowTitle("Завершение...");
loader->show();
QCoreApplication::processEvents();
QEventLoop loop;
QObject::connect(networkService, &NetworkService::finished, &loop, &QEventLoop::quit);
// Таймаут на случай если сервер недоступен
QTimer::singleShot(10000, &loop, &QEventLoop::quit);
networkService->stop();
loop.exec();
loader->close();
delete loader;
});
// Сигнал эмитируется из фонового потока → Qt::QueuedConnection доставит его в главный поток

View File

@ -1,11 +1,12 @@
-- ============================================================
-- Версия 1 (user_version = 1): начальная схема
-- Схема БД ARCDeskProject
-- ============================================================
CREATE TABLE IF NOT EXISTS devices
(
id TEXT PRIMARY KEY,
api_id TEXT,
adb_serial TEXT,
status TEXT,
name TEXT,
android TEXT,
@ -17,7 +18,7 @@ CREATE TABLE IF NOT EXISTS devices
image BLOB
);
CREATE TABLE IF NOT EXISTS applications
CREATE TABLE IF NOT EXISTS bank_profiles
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
@ -29,12 +30,18 @@ CREATE TABLE IF NOT EXISTS applications
status TEXT DEFAULT 'off',
install TEXT DEFAULT 'no',
comment TEXT,
phone TEXT DEFAULT '',
email TEXT DEFAULT '',
full_name TEXT DEFAULT '',
bank_profile_id TEXT DEFAULT '',
currency TEXT DEFAULT '',
amount REAL DEFAULT 0,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, code)
);
-- app_code: код банка ("ozon", "black") — не путать с человекочитаемым name
CREATE TABLE IF NOT EXISTS accounts
CREATE TABLE IF NOT EXISTS materials
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
@ -45,6 +52,8 @@ CREATE TABLE IF NOT EXISTS accounts
currency TEXT DEFAULT '',
update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
status TEXT DEFAULT 'active',
card_number TEXT DEFAULT '',
material_id TEXT DEFAULT '',
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, app_code, last_numbers)
);
@ -70,14 +79,15 @@ CREATE TABLE IF NOT EXISTS transactions
update_time DATETIME,
complete_time DATETIME,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
receipt_image BLOB,
FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS events
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
external_id INTEGER,
external_id TEXT,
device_id TEXT,
amount REAL,
phone TEXT,
@ -88,15 +98,46 @@ CREATE TABLE IF NOT EXISTS events
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,
UNIQUE (external_id)
json TEXT DEFAULT '',
sent_to_server INTEGER DEFAULT 0,
screenshot BLOB,
FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE
);
-- ============================================================
-- Версия 2 (user_version = 2): currency + rename app_name→app_code
-- ============================================================
CREATE TABLE IF NOT EXISTS banks
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
alias TEXT UNIQUE,
name TEXT,
nspk_name TEXT
);
-- Для существующих БД (применяется автоматически при старте):
-- ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT '';
-- ALTER TABLE accounts RENAME COLUMN app_name TO app_code;
CREATE TABLE IF NOT EXISTS app_logs
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL DEFAULT '',
device_id TEXT NOT NULL DEFAULT '',
message TEXT NOT NULL,
screenshot BLOB,
timestamp TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC);
CREATE TABLE IF NOT EXISTS general_logs
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
level TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT '',
message TEXT NOT NULL,
timestamp TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_general_logs_id ON general_logs (id DESC);
CREATE TABLE IF NOT EXISTS settings
(
key TEXT PRIMARY KEY,
value TEXT
);

View File

@ -1,44 +0,0 @@
#pragma once
#include <QDateTime>
enum class AccountStatus {
Active,
Off
};
inline QString accountStatusToString(const AccountStatus status) {
switch (status) {
case AccountStatus::Active: return "active";
case AccountStatus::Off: return "off";
default: return "active";
}
}
inline QString accountStatusToUiString(const AccountStatus status) {
switch (status) {
case AccountStatus::Active: return "в работе";
case AccountStatus::Off: return "выключена";
default: return "в работе";
}
}
inline AccountStatus accountStatusFromString(const QString &str) {
if (str == "active") return AccountStatus::Active;
if (str == "off") return AccountStatus::Off;
return AccountStatus::Active;
}
struct AccountInfo {
int id = -1;
QString deviceId; // code
QString appCode;
QString lastNumbers;
QString cardNumber; // full 16-digit card number
QString materialId;
QString currency;
QString description;
double amount = 0.0;
QDateTime updateTime;
AccountStatus status = AccountStatus::Active;
};

View File

@ -4,7 +4,7 @@
#include <QByteArray>
#include <QDateTime>
struct ApplicationInfo {
struct BankProfileInfo {
int id = -1;
bool install = false;
bool pinCodeChecked = false;

View File

@ -3,8 +3,9 @@
#include <QDateTime>
struct DeviceInfo {
QString id; // ADB device id (e.g. "emulator-5554")
QString id; // android_id (unique device identifier)
QString apiId; // UUID from backend API (DeviceResponseModel.id)
QString adbSerial; // ADB serial (e.g. "emulator-5554", "7dfc444d43fbeab7")
QString status;
QString name;
QString android;

44
models/db/MaterialInfo.h Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include <QDateTime>
enum class MaterialStatus {
Active,
Off
};
inline QString materialStatusToString(const MaterialStatus status) {
switch (status) {
case MaterialStatus::Active: return "active";
case MaterialStatus::Off: return "off";
default: return "active";
}
}
inline QString materialStatusToUiString(const MaterialStatus status) {
switch (status) {
case MaterialStatus::Active: return "в работе";
case MaterialStatus::Off: return "выключена";
default: return "в работе";
}
}
inline MaterialStatus materialStatusFromString(const QString &str) {
if (str == "active") return MaterialStatus::Active;
if (str == "off") return MaterialStatus::Off;
return MaterialStatus::Active;
}
struct MaterialInfo {
int id = -1;
QString deviceId; // code
QString appCode;
QString lastNumbers;
QString cardNumber; // full 16-digit card number
QString materialId;
QString currency;
QString description;
double amount = 0.0;
QDateTime updateTime;
MaterialStatus status = MaterialStatus::Active;
};

View File

@ -19,7 +19,8 @@
#include <db/DeviceInfo.h>
#include "adb/AdbUtils.h"
#include "dao/ApplicationDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/SettingsDAO.h"
DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) {
@ -182,6 +183,7 @@ QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) {
QJsonObject body;
body["desktop_id"] = desktopId;
body["bank_profile_id"] = QJsonValue::Null;
body["status"] = "ONLINE";
body["name"] = device.name;
body["screen_width"] = device.screenWidth;
@ -239,27 +241,44 @@ void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray
}
void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) {
// Находим первый активный bank_profile для этого устройства
QJsonValue bankProfileId = QJsonValue::Null;
const QList<BankProfileInfo> apps = BankProfileDAO::getAllApplicationsByDeviceId(device.id);
for (const BankProfileInfo &app : apps) {
if (!app.bankProfileId.isEmpty() && app.status == "active") {
bankProfileId = app.bankProfileId;
break;
}
}
QJsonObject body;
body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE";
body["bank_profile_id"] = bankProfileId;
body["battery"] = device.battery;
body["update_time"] = QDateTime::currentSecsSinceEpoch();
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 (enable) return; // не включаем автоматически при появлении устройства
const QList<BankProfileInfo> apps = BankProfileDAO::getAllApplicationsByDeviceId(deviceId);
for (const BankProfileInfo &app : apps) {
if (app.bankProfileId.isEmpty()) continue;
if (app.status != "active") continue;
// При включении — только если приложение активно в системе
if (enable && app.status != "active") continue;
apiPatch("/api/v1/profile/bank_profile/" + app.bankProfileId + "/false", QJsonObject());
const QString enableStr = enable ? "true" : "false";
const QString path = "/api/v1/profile/bank_profile/" + app.bankProfileId + "/" + enableStr;
apiPatch(path, QJsonObject());
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(deviceId, app.code);
for (const MaterialInfo &mat : materials) {
if (mat.materialId.isEmpty()) continue;
apiPatch("/api/v1/profile/material/" + mat.materialId + "/false", QJsonObject());
}
}
BankProfileDAO::deactivateAppsForDevice(deviceId);
}
QJsonValue DeviceScreener::apiGet(const QString &path) {
if (!m_manager) m_manager = new QNetworkAccessManager(this);
@ -304,24 +323,39 @@ void DeviceScreener::start() {
QList<QPair<QString, QString> > devices = readAdbDevices();
QStringList onlineIds;
QList<DeviceInfo> deviceInfos;
for (const auto &[id, status]: devices) {
for (const auto &[adbSerial, status]: devices) {
DeviceInfo device;
device.id = id;
bool isChecked = true;
if (status == "device") {
device = readDeviceInfo(adbSerial);
// Проверяем: есть ли уже устройство с таким id в БД (с apiId)
const DeviceInfo existingById = DeviceDAO::getDeviceById(device.id);
if (!existingById.id.isEmpty() && !existingById.apiId.isEmpty()) {
device.apiId = existingById.apiId;
} else {
// Маппинг с восстановленным устройством — только по name (точное совпадение)
const DeviceInfo restored = DeviceDAO::findByName(device.name);
if (!restored.id.isEmpty() && restored.id != device.id && !restored.apiId.isEmpty()) {
device.apiId = restored.apiId;
DeviceDAO::deleteDevice(restored.id);
qDebug() << "[DeviceScreener] matched restored device:" << restored.id << "->" << device.id
<< "apiId:" << device.apiId;
}
}
onlineIds << device.id;
const bool isChecked = prevOnline.contains(id);
device = readDeviceInfo(id, isChecked);
device.image = takeScreenshot(id);
isChecked = prevOnline.contains(device.id);
device.image = takeScreenshot(adbSerial);
if (!device.image.isEmpty()) {
if (!DeviceDAO::updateImage(id, device.image)) {
if (!DeviceDAO::updateImage(device.id, device.image)) {
qDebug() << "Cant update screenshot";
}
}
if (!isChecked) {
// Новое устройство — включаем active профили
toggleBankProfilesOnApi(id, true);
}
} else {
device.id = adbSerial;
}
device.status = status;
device.updateTime = QDateTime::currentDateTime();
@ -330,6 +364,16 @@ void DeviceScreener::start() {
if (!DeviceDAO::upsertDevice(device)) {
qDebug() << "Cant update device";
}
if (!isChecked) {
// Новое устройство — проверяем приложения (после upsert, чтобы FK не падал)
QSet<QString> apps = AdbUtils::getListApps(adbSerial);
if (!BankProfileDAO::checkInstalledApps(device.id, apps)) {
qWarning() << "Cant check installed apps";
} else {
qDebug() << "Apps checked:" << device.id;
}
}
}
// Синхронизация с API
@ -361,10 +405,17 @@ void DeviceScreener::start() {
qDebug() << "Cant mark devices offline";
}
// Было онлайн в БД, сейчас офлайн — выключаем профили на сервере
// Было онлайн в БД, сейчас офлайн — выключаем профили и обновляем статус на сервере
for (const QString &id : prevOnline) {
if (!onlineIds.contains(id)) {
toggleBankProfilesOnApi(id, false);
// Отправляем OFFLINE статус на сервер
const DeviceInfo offlineDevice = DeviceDAO::getDeviceById(id);
if (!offlineDevice.apiId.isEmpty()) {
DeviceInfo offlineCopy = offlineDevice;
offlineCopy.status = "offline";
updateDeviceOnApi(offlineCopy);
}
}
}
@ -452,9 +503,8 @@ QList<QPair<QString, QString> > DeviceScreener::readAdbDevices() {
static const QRegularExpression screenRegex(R"(Physical size: (\d+)x(\d+))");
static const QRegularExpression batteryRegex(R"(level:\s*(\d+))");
DeviceInfo DeviceScreener::readDeviceInfo(const QString &deviceId, const bool isChecked) {
DeviceInfo DeviceScreener::readDeviceInfo(const QString &adbSerial) {
DeviceInfo info;
info.id = deviceId;
info.screenWidth = 0;
info.screenHeight = 0;
info.battery = 0;
@ -462,12 +512,17 @@ DeviceInfo DeviceScreener::readDeviceInfo(const QString &deviceId, const bool is
auto runAdbCommand = [&](const QStringList &args) -> QString {
QProcess process;
process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath());
process.start(AdbUtils::adbPath(), QStringList{"-s", deviceId} + args);
process.start(AdbUtils::adbPath(), QStringList{"-s", adbSerial} + args);
process.waitForFinished();
return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
};
info.name = runAdbCommand({"shell", "getprop", "ro.product.model"});
const QString androidId = runAdbCommand({"shell", "settings", "get", "secure", "android_id"});
const QString model = runAdbCommand({"shell", "getprop", "ro.product.model"});
info.id = androidId.isEmpty() ? adbSerial : androidId;
info.adbSerial = adbSerial;
info.name = model + "-" + info.id;
info.android = runAdbCommand({"shell", "getprop", "ro.build.version.release"});
// Размер экрана
@ -485,15 +540,6 @@ DeviceInfo DeviceScreener::readDeviceInfo(const QString &deviceId, const bool is
info.battery = match.captured(1).toInt();
}
if (!isChecked) {
QSet<QString> apps = AdbUtils::getListApps(deviceId);
if (!ApplicationDAO::checkInstalledApps(deviceId, apps)) {
qWarning() << "Cant check installed apps";
} else {
qDebug() << "Apps checked: " + deviceId;
}
}
info.updateTime = QDateTime::currentDateTime();
return info;
}

View File

@ -33,7 +33,7 @@ private:
static QByteArray takeScreenshot(const QString &deviceId);
static DeviceInfo readDeviceInfo(const QString &deviceId, bool isChecked);
static DeviceInfo readDeviceInfo(const QString &adbSerial);
void postDevicesData(const QList<DeviceInfo> &devices);

View File

@ -10,13 +10,13 @@
#include <QTimer>
#include <QTimeZone>
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/BankDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/EventDAO.h"
#include "dao/TransactionDAO.h"
#include "db/ApplicationInfo.h"
#include "db/BankProfileInfo.h"
#include "adb/AdbUtils.h"
#include "net/NetworkService.h"
#include "black/GetLastTransactionsScript.h"
@ -50,7 +50,7 @@ void EventHandler::start() {
// Запрашиваем новые задачи только для устройств без pending events
const QSet<QString> busyDevices = EventDAO::getDevicesWithPendingEvents();
const QStringList materialIds = AccountDAO::getActiveMaterialIdsExcludingDevices(busyDevices);
const QStringList materialIds = MaterialDAO::getActiveMaterialIdsExcludingDevices(busyDevices);
if (!materialIds.isEmpty()) {
m_network->getTasks(materialIds);
}
@ -99,13 +99,18 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
}
// Ищем аккаунт по material_id напрямую
const AccountInfo account = AccountDAO::findByMaterialId(materialId);
const MaterialInfo account = MaterialDAO::findByMaterialId(materialId);
if (account.id == -1) {
qWarning() << "[EventHandler] Account not found for bank:" << bankName << "material_id:" << materialId;
sendTaskError(typeStr, materialId, bankName, "Account not found for material_id: " + materialId);
continue;
}
// FETCH_PROFILE: отменяем старые ожидающие задачи такого же типа на этом устройстве
if (eventType == EventType::FetchProfile) {
EventDAO::cancelWaitingByType(account.deviceId, EventType::FetchProfile);
}
EventInfo event;
event.status = EventStatus::Wait;
event.type = eventType;
@ -227,18 +232,44 @@ static void sendEventStatus(const QString &deviceId, const QString &externalId,
}
void EventHandler::updateEventThread(const QString &key, const EventInfo &event, const QString &result) {
// FETCH_TRANSACTIONS: если транзакция не найдена и прошло менее 10 минут — повторяем
if (!result.isEmpty() && event.type == EventType::FetchTransactions
&& result.startsWith("Transaction not found")) {
const qint64 elapsedSecs = event.timestamp.secsTo(QDateTime::currentDateTimeUtc());
if (elapsedSecs < 600) {
qDebug() << "[EventHandler] FETCH_TRANSACTIONS not found, requeueing (elapsed:" << elapsedSecs << "s)";
EventDAO::updateEvent(event.id, EventStatus::Wait, result);
m_threads.remove(key);
// Повтор через 15 секунд
QTimer::singleShot(15000, this, [this, key]() {
processNextTask(key);
});
return;
}
qWarning() << "[EventHandler] FETCH_TRANSACTIONS timeout (elapsed:" << elapsedSecs << "s), sending error";
}
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 {
if (newStatus == EventStatus::Error) {
// Сохраняем скриншот устройства при ошибке
if (!event.deviceId.isEmpty()) {
const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId);
const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial;
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(adbSerial);
if (!screenshot.isEmpty()) {
EventDAO::updateScreenshot(event.id, screenshot);
}
}
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result);
EventDAO::markSentToServer(event.id);
}
if (newStatus != EventStatus::Error) {
const AccountInfo account = AccountDAO::getAccountById(event.accountId);
const MaterialInfo account = MaterialDAO::getAccountById(event.accountId);
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}
};
@ -353,7 +384,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
processNextTask(key);
}
void EventHandler::startThreadForTask(const AccountInfo &account, const EventInfo &event) {
void EventHandler::startThreadForTask(const MaterialInfo &account, const EventInfo &event) {
const auto thr = new QThread(this);
const QString key = account.deviceId;
@ -373,17 +404,27 @@ void EventHandler::startThreadForTask(const AccountInfo &account, const EventInf
};
const QString &appCode = account.appCode;
const QString &deviceId = account.deviceId;
// Скриптам нужен ADB serial для команд, не android_id
const DeviceInfo device = DeviceDAO::getDeviceById(account.deviceId);
const QString adbSerial = device.adbSerial.isEmpty() ? account.deviceId : device.adbSerial;
if (event.type == EventType::FetchProfile) {
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(deviceId, appCode));
else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(deviceId, appCode));
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true));
else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(adbSerial, appCode));
else qWarning() << "[EventHandler] FETCH_PROFILE: unknown appCode:" << appCode;
}
if (event.type == EventType::FetchTransactions) {
if (appCode == "ozon") setup(new Ozon::GetLastTransactionsScript(deviceId, appCode));
else if (appCode == "black") setup(new Black::GetLastTransactionsScript(deviceId, appCode));
// Парсим amount и created_at из body задачи
const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object();
const QJsonObject body = taskJson["body"].toObject();
const double searchAmount = body["amount"].toDouble() / 100.0; // копейки → рубли
const QDateTime searchTime = QDateTime::fromString(body["created_at"].toString(), Qt::ISODate);
if (appCode == "ozon")
setup(new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime));
else if (appCode == "black")
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
}
@ -448,8 +489,8 @@ void EventHandler::processNextTask(const QString &deviceId) {
continue;
}
const AccountInfo account = AccountDAO::getAccountById(event.accountId);
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, account.appCode);
const MaterialInfo account = MaterialDAO::getAccountById(event.accountId);
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, account.appCode);
if (app.status != "active") {
qWarning() << "[EventHandler] Bank profile disabled for" << account.appCode << "device:" << deviceId;
EventDAO::updateEvent(event.id, EventStatus::Error, "Bank profile is disabled");

View File

@ -4,7 +4,7 @@
#include <QObject>
#include <QTimer>
#include "db/AccountInfo.h"
#include "db/MaterialInfo.h"
#include "db/EventInfo.h"
class NetworkService;
@ -39,7 +39,7 @@ private:
void updateEventThread(const QString &key, const EventInfo &event, const QString &result);
void startThreadForTask(const AccountInfo &account, const EventInfo &event);
void startThreadForTask(const MaterialInfo &account, const EventInfo &event);
void processNextTask(const QString &deviceId);
};

View File

@ -11,8 +11,9 @@
#include <QTimer>
#include <QTimeZone>
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/BankDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/EventDAO.h"
#include "dao/SettingsDAO.h"
@ -181,7 +182,7 @@ void NetworkService::getPayments() {
QString deviceId = obj["deviceId"].toString();
QString appName = obj["appName"].toString();
QString lastNumbers = obj["lastNumbers"].toString();
AccountInfo account = AccountDAO::findAppAccount(deviceId, appName, lastNumbers);
MaterialInfo account = MaterialDAO::findAppAccount(deviceId, appName, lastNumbers);
eventInfo.accountId = account.id;
eventInfo.deviceId = account.deviceId;
@ -291,10 +292,37 @@ void NetworkService::loginAndSetup() {
const qint64 expiresIn = static_cast<qint64>(loginData["expires_in"].toDouble());
SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn);
// Шаг 2: проверяем есть ли desktop_id
const QString desktopId = SettingsDAO::get("desktop_id");
// Шаг 2: получаем актуальный desktop с сервера
QString desktopId = SettingsDAO::get("desktop_id");
// Всегда проверяем список десктопов — desktop_id мог устареть или быть удалён
const QJsonValue existingDesktops = getJson(m_apiBase + m_pathDesktopCreate, true);
if (!existingDesktops.isUndefined() && !existingDesktops.isNull() && existingDesktops.isArray()) {
const QJsonArray desktopsArray = existingDesktops.toArray();
QString foundId;
QString foundProfileId;
for (const QJsonValue &dVal : desktopsArray) {
const QJsonObject d = dVal.toObject();
if (d["hidden"].toBool(false)) continue;
foundId = d["id"].toString();
foundProfileId = d["profile_id"].toString();
if (!foundId.isEmpty()) break;
}
if (!foundId.isEmpty() && foundId != desktopId) {
// Нашли актуальный десктоп, отличается от сохранённого — обновляем
qDebug() << "[loginAndSetup] desktop_id updated:" << desktopId << "->" << foundId;
desktopId = foundId;
SettingsDAO::set("desktop_id", desktopId);
SettingsDAO::set("profile_id", foundProfileId);
} else if (!foundId.isEmpty()) {
qDebug() << "[loginAndSetup] desktop_id confirmed:" << desktopId;
}
}
if (desktopId.isEmpty()) {
// Шаг 3: создаём desktop
// Десктопов на сервере нет — создаём новый
QJsonObject desktopBody;
desktopBody["blue_token"] = QJsonValue::Null;
desktopBody["name"] = QJsonValue::Null;
@ -310,13 +338,38 @@ void NetworkService::loginAndSetup() {
}
const QJsonObject desktopData = desktopResult.toObject();
SettingsDAO::set("desktop_id", desktopData["id"].toString());
desktopId = desktopData["id"].toString();
SettingsDAO::set("desktop_id", desktopId);
SettingsDAO::set("profile_id", desktopData["profile_id"].toString());
}
// Шаг 4: синхронизация устройств и профилей
// Шаг 3: синхронизация устройств с сервера
syncDevicesFromServer(desktopId);
// Шаг 4: синхронизация банковских профилей и материалов с сервера
syncBankProfilesFromServer();
// Шаг 5: загрузка справочника банков если пустой
syncBanksIfEmpty();
// Шаг 6: синхронизация статусов профилей/материалов с учётом онлайн-устройств
syncCurrentProfile();
// Шаг 7: отправляем OFFLINE статус всех устройств (DeviceScreener обновит на ONLINE при обнаружении)
{
const QList<DeviceInfo> allDevices = DeviceDAO::getAllDevices(false);
for (const DeviceInfo &d : allDevices) {
if (d.apiId.isEmpty()) continue;
QJsonObject body;
body["status"] = "OFFLINE";
body["bank_profile_id"] = QJsonValue::Null;
body["battery"] = d.battery;
body["update_time"] = QDateTime::currentSecsSinceEpoch();
patchJson(m_apiBase + m_pathDevice + d.apiId, body, true);
qDebug() << "[loginAndSetup] sent OFFLINE for device" << d.id << d.apiId;
}
}
emit finishedWithResult(1);
emit finished();
}
@ -412,6 +465,49 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod
return result;
}
QJsonValue NetworkService::deleteJson(const QString &path, bool withAuth) {
const QUrl url(path);
QNetworkRequest request(url);
if (withAuth && !m_accessToken.isEmpty()) {
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
}
QNetworkReply *reply = m_manager->deleteResource(request);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
QJsonValue result;
if (!timeout.isActive()) {
qCritical() << "Таймаут запроса (DELETE):" << path;
reply->abort();
result = QJsonValue::Undefined;
} else if (reply->error() != QNetworkReply::NoError) {
qCritical() << "Network error (DELETE):" << reply->errorString() << path;
result = QJsonValue::Undefined;
} else {
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();
if (root["success"].toBool()) {
result = root["data"];
} else {
qCritical() << "API error (DELETE):" << root;
result = QJsonValue::Null;
}
}
}
reply->deleteLater();
return result;
}
QJsonValue NetworkService::getJson(const QString &path, bool withAuth) {
const QUrl url(path);
QNetworkRequest request(url);
@ -457,6 +553,255 @@ QJsonValue NetworkService::getJson(const QString &path, bool withAuth) {
return result;
}
void NetworkService::syncBankProfilesFromServer() {
const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true);
if (profileResult.isUndefined() || profileResult.isNull()) {
qWarning() << "[syncBankProfilesFromServer] failed to get current_profile";
return;
}
const QJsonObject profileData = profileResult.toObject();
const QJsonArray bankProfiles = profileData["bank_profiles"].toArray();
// Маппинг apiId → локальный device_id
const QList<DeviceInfo> allDevices = DeviceDAO::getAllDevices(false);
QMap<QString, QString> apiIdToLocalId;
for (const DeviceInfo &d : allDevices) {
if (!d.apiId.isEmpty()) {
apiIdToLocalId[d.apiId] = d.id;
}
}
const QSettings configSettings("config.ini", QSettings::IniFormat);
for (const QJsonValue &bpVal : bankProfiles) {
const QJsonObject bp = bpVal.toObject();
const QString bpId = bp["id"].toString();
const QString bankName = bp["bank_name"].toString();
const QString bpState = bp["state"].toString();
const QString phone = bp["phone_number"].toString();
const QString firstName = bp["first_name"].toString();
const QString middleName = bp["middle_name"].toString();
const QString lastName = bp["last_name"].toString();
const QString currency = bp["currency"].isNull() ? "" : QString::number(bp["currency"].toInt());
const QString serverDeviceId = bp["device_id"].toString(); // API UUID устройства
if (bpId.isEmpty() || bankName.isEmpty()) continue;
// Определяем локальный device_id по API UUID
const QString localDeviceId = apiIdToLocalId.value(serverDeviceId);
if (localDeviceId.isEmpty()) {
qDebug() << "[syncBankProfilesFromServer] device not found locally for api_id" << serverDeviceId << ", skipping bp" << bpId;
continue;
}
const QString fullName = (firstName + " " + (middleName.isEmpty() ? "" : middleName + " ") + lastName).trimmed();
BankProfileInfo app = BankProfileDAO::getApplication(localDeviceId, bankName);
if (app.id < 0) {
// Приложение не обнаружено на устройстве — создаём запись
BankProfileInfo newApp;
newApp.deviceId = localDeviceId;
newApp.code = bankName;
newApp.name = configSettings.value(bankName + "/name", bankName).toString();
newApp.package = configSettings.value(bankName + "/android_package_name").toString();
newApp.status = (bpState == "disabled") ? "off" : "active";
newApp.install = false;
newApp.pinCodeChecked = false;
newApp.bankProfileId = bpId;
newApp.fullName = fullName;
newApp.phone = phone;
newApp.currency = currency.isEmpty()
? configSettings.value(bankName + "/currency").toString()
: currency;
if (BankProfileDAO::upsertApplication(newApp)) {
app = BankProfileDAO::getApplication(localDeviceId, bankName);
qDebug() << "[syncBankProfilesFromServer] created app" << app.id << bankName << "device" << localDeviceId;
} else {
qWarning() << "[syncBankProfilesFromServer] failed to create app" << bankName << "device" << localDeviceId;
continue;
}
} else {
// Application существует — привязываем bank_profile_id если нужно
if (app.bankProfileId != bpId) {
BankProfileDAO::updateBankProfileId(app.id, bpId);
qDebug() << "[syncBankProfilesFromServer] linked bp" << bpId << "to app" << app.id;
}
if (app.fullName.isEmpty() || app.phone.isEmpty()) {
BankProfileDAO::updateProfileInfo(app.id, fullName, phone);
}
}
// Синхронизируем materials → accounts
const QJsonArray materials = bp["materials"].toArray();
for (const QJsonValue &matVal : materials) {
const QJsonObject mat = matVal.toObject();
const QString matId = mat["id"].toString();
const QString cardNumber = mat["card_number"].toString();
if (matId.isEmpty()) continue;
// Уже есть account с этим material_id — пропускаем
const MaterialInfo existing = MaterialDAO::findByMaterialId(matId);
if (existing.id >= 0) continue;
const QString lastNumbers = cardNumber.length() >= 4 ? cardNumber.right(4) : cardNumber;
// Ищем существующий account по last_numbers
MaterialInfo acc = MaterialDAO::findAppAccount(localDeviceId, bankName, lastNumbers);
if (acc.id >= 0) {
if (acc.materialId.isEmpty()) {
MaterialDAO::updateMaterialId(acc.id, matId);
if (acc.cardNumber.isEmpty() && !cardNumber.isEmpty()) {
MaterialDAO::updateCardNumber(acc.id, cardNumber);
}
qDebug() << "[syncBankProfilesFromServer] linked material" << matId << "to account" << acc.id;
}
} else if (!lastNumbers.isEmpty()) {
// Account не существует — создаём
MaterialInfo newAcc;
newAcc.deviceId = localDeviceId;
newAcc.appCode = bankName;
newAcc.lastNumbers = lastNumbers;
newAcc.cardNumber = cardNumber;
newAcc.materialId = matId;
newAcc.currency = currency.isEmpty()
? configSettings.value(bankName + "/currency").toString()
: currency;
newAcc.status = MaterialStatus::Active;
newAcc.updateTime = QDateTime::currentDateTime();
if (MaterialDAO::upsertAccount(newAcc)) {
qDebug() << "[syncBankProfilesFromServer] created account for material" << matId << "card" << lastNumbers;
} else {
qWarning() << "[syncBankProfilesFromServer] failed to create account for material" << matId;
}
}
}
}
qDebug() << "[syncBankProfilesFromServer] done";
}
void NetworkService::syncBanksIfEmpty() {
const QList<BankInfo> existing = BankDAO::getAll();
if (!existing.isEmpty()) {
qDebug() << "[syncBanksIfEmpty] banks table has" << existing.size() << "entries, skipping";
return;
}
qDebug() << "[syncBanksIfEmpty] banks table empty, downloading...";
QNetworkRequest request(QUrl("https://api.brtservice.io/api/rest/info/"));
QNetworkReply *reply = m_manager->get(request);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
if (!timeout.isActive()) {
qWarning() << "[syncBanksIfEmpty] timeout";
reply->abort();
reply->deleteLater();
return;
}
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[syncBanksIfEmpty] error:" << reply->errorString();
reply->deleteLater();
return;
}
const QByteArray data = reply->readAll();
reply->deleteLater();
const QJsonDocument doc = QJsonDocument::fromJson(data);
QJsonArray array;
if (doc.isArray()) {
array = doc.array();
} else if (doc.isObject() && doc.object().contains("data")) {
array = doc.object()["data"].toArray();
}
QList<BankInfo> banks;
for (const QJsonValue &val : array) {
const QJsonObject obj = val.toObject();
BankInfo bank;
bank.alias = obj["alias"].toString();
bank.name = obj["name"].toString();
bank.nspkName = obj["nspk_name"].toString();
if (!bank.alias.isEmpty()) {
banks.append(bank);
}
}
if (!banks.isEmpty()) {
BankDAO::replaceAll(banks);
qDebug() << "[syncBanksIfEmpty] synced" << banks.size() << "banks";
} else {
qWarning() << "[syncBanksIfEmpty] no banks in response";
}
}
void NetworkService::syncDevicesFromServer(const QString &desktopId) {
if (desktopId.isEmpty()) return;
const QJsonValue devicesResult = getJson(m_apiBase + m_pathDevice + desktopId, true);
if (devicesResult.isUndefined() || devicesResult.isNull() || !devicesResult.isArray()) {
qWarning() << "[syncDevicesFromServer] failed to get devices for desktop" << desktopId;
return;
}
const QJsonArray devicesArray = devicesResult.toArray();
QSet<QString> processedNames; // защита от дублей с сервера
for (const QJsonValue &devVal : devicesArray) {
const QJsonObject dev = devVal.toObject();
const QString apiId = dev["id"].toString();
const QString name = dev["name"].toString();
if (apiId.isEmpty()) continue;
// Пропускаем дубли по name
if (processedNames.contains(name)) {
qDebug() << "[syncDevicesFromServer] skipping duplicate:" << apiId << name;
continue;
}
processedNames.insert(name);
// Проверяем: есть ли уже устройство с таким name в локальной БД
const DeviceInfo existing = DeviceDAO::findByName(name);
if (!existing.id.isEmpty() && !existing.apiId.isEmpty()) {
continue;
}
if (!existing.id.isEmpty() && existing.apiId.isEmpty()) {
DeviceDAO::updateApiId(existing.id, apiId);
qDebug() << "[syncDevicesFromServer] linked apiId" << apiId << "to device" << existing.id;
continue;
}
// Устройства нет в БД — создаём одну запись (DeviceScreener потом подхватит)
DeviceInfo deviceInfo;
deviceInfo.apiId = apiId;
deviceInfo.name = name;
deviceInfo.status = dev["status"].toString();
deviceInfo.screenWidth = dev["screen_width"].toInt();
deviceInfo.screenHeight = dev["screen_height"].toInt();
deviceInfo.battery = dev["battery"].toInt();
deviceInfo.id = apiId;
if (DeviceDAO::upsertDevice(deviceInfo)) {
qDebug() << "[syncDevicesFromServer] restored device:" << apiId << name;
} else {
qWarning() << "[syncDevicesFromServer] failed to restore device:" << apiId;
}
}
}
void NetworkService::syncCurrentProfile() {
// 1. Определяем онлайн-устройства по БД (DeviceScreener обновит позже)
const QList<DeviceInfo> allDevices = DeviceDAO::getAllDevices(false);
@ -492,8 +837,8 @@ void NetworkService::syncCurrentProfile() {
const QString serverBpState = bp["state"].toString(); // "enabled" / "disabled"
// Находим applications с этим bank_profile_id
const QList<ApplicationInfo> apps = ApplicationDAO::findAllByBankProfileId(bpId);
for (const ApplicationInfo &app : apps) {
const QList<BankProfileInfo> apps = BankProfileDAO::findAllByBankProfileId(bpId);
for (const BankProfileInfo &app : apps) {
const bool deviceOnline = onlineDeviceIds.contains(app.deviceId);
const QString localStatus = app.status; // "active" / "off"
const QString serverStatus = (serverBpState == "disabled") ? "off" : "active";
@ -521,11 +866,11 @@ void NetworkService::syncCurrentProfile() {
const QString matId = mat["id"].toString();
const QString serverMatState = mat["state"].toString();
const AccountInfo account = AccountDAO::findByMaterialId(matId);
const MaterialInfo account = MaterialDAO::findByMaterialId(matId);
if (account.id < 0) continue; // не найден в локальной БД
const bool deviceOnline = onlineDeviceIds.contains(account.deviceId);
const QString localStatus = accountStatusToString(account.status);
const QString localStatus = materialStatusToString(account.status);
const QString serverStatus = (serverMatState == "disabled") ? "off" : "active";
if (!deviceOnline) {
@ -559,7 +904,7 @@ void NetworkService::postBankProfile() {
const QString bankProfileId = data["id"].toString();
const int appId = m_extraData.value("app_id").toInt();
if (!bankProfileId.isEmpty() && appId > 0) {
ApplicationDAO::updateBankProfileId(appId, bankProfileId);
BankProfileDAO::updateBankProfileId(appId, bankProfileId);
qDebug() << "[NetworkService] postBankProfile saved bank_profile_id:" << bankProfileId;
}
}
@ -616,7 +961,7 @@ void NetworkService::postMaterial() {
const QString materialId = result.toObject()["id"].toString();
const int accountId = m_extraData.value("account_id").toInt();
if (!materialId.isEmpty() && accountId > 0) {
AccountDAO::updateMaterialId(accountId, materialId);
MaterialDAO::updateMaterialId(accountId, materialId);
}
qDebug() << "[NetworkService] postMaterial ok, card:" << body["card_number"].toString() << "material_id:" << materialId;
}
@ -802,7 +1147,7 @@ void NetworkService::getTasks(const QStringList &materialIds) {
QUrl url(m_apiBase + m_pathGetTasks);
QUrlQuery urlQuery;
const QStringList ids = materialIds.isEmpty() ? AccountDAO::getActiveMaterialIds() : materialIds;
const QStringList ids = materialIds.isEmpty() ? MaterialDAO::getActiveMaterialIds() : materialIds;
if (ids.isEmpty()) {
m_fetchingTasks = false;
return;
@ -903,8 +1248,62 @@ void NetworkService::start() {
scheduleNextRefresh();
}
void NetworkService::deleteDeviceFromServer() {
if (!ensureValidToken()) { emit finished(); return; }
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
if (device.apiId.isEmpty()) {
qWarning() << "[deleteDeviceFromServer] no api_id for device" << m_deviceId;
// Удаляем только локально
DeviceDAO::deleteDevice(m_deviceId);
emit finished();
return;
}
const QJsonValue result = deleteJson(m_apiBase + m_pathDevice + device.apiId, true);
if (result.isUndefined()) {
qWarning() << "[deleteDeviceFromServer] failed to delete device on server:" << device.apiId;
} else {
qDebug() << "[deleteDeviceFromServer] deleted device on server:" << device.apiId;
}
DeviceDAO::deleteDevice(m_deviceId);
emit finished();
}
void NetworkService::disableAllProfilesOnServer() {
if (!ensureValidToken()) return;
const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true);
if (profileResult.isUndefined() || profileResult.isNull()) return;
const QJsonArray bankProfiles = profileResult.toObject()["bank_profiles"].toArray();
for (const QJsonValue &bpVal : bankProfiles) {
const QJsonObject bp = bpVal.toObject();
const QString bpId = bp["id"].toString();
const QString bpState = bp["state"].toString();
if (bpId.isEmpty() || bpState == "disabled") continue;
patchJson(m_apiBase + m_pathBankProfile + "/" + bpId + "/false", QJsonObject(), true);
qDebug() << "[shutdown] disabled bank_profile" << bpId;
const QJsonArray materials = bp["materials"].toArray();
for (const QJsonValue &matVal : materials) {
const QJsonObject mat = matVal.toObject();
const QString matId = mat["id"].toString();
const QString matState = mat["state"].toString();
if (matId.isEmpty() || matState == "disabled") continue;
patchJson(m_apiBase + m_pathMaterial + "/" + matId + "/false", QJsonObject(), true);
qDebug() << "[shutdown] disabled material" << matId;
}
}
}
void NetworkService::stop() {
m_running = false;
// Эмитируем finished() в потоке NetworkService, чтобы корректно завершить QThread
QMetaObject::invokeMethod(this, [this]() { emit finished(); }, Qt::QueuedConnection);
QMetaObject::invokeMethod(this, [this]() {
disableAllProfilesOnServer();
emit finished();
}, Qt::QueuedConnection);
}

View File

@ -60,6 +60,8 @@ public slots:
void start();
void stop();
void disableAllProfilesOnServer();
void deleteDeviceFromServer();
signals:
void finished();
@ -103,8 +105,12 @@ private:
QJsonValue getJson(const QString &path, bool withAuth = false);
QJsonValue postJson(const QString &path, const QJsonObject &body, bool withAuth = false);
QJsonValue patchJson(const QString &path, const QJsonObject &body, bool withAuth = false);
QJsonValue deleteJson(const QString &path, bool withAuth = false);
bool refreshToken();
bool ensureValidToken();
void scheduleNextRefresh();
void syncDevicesFromServer(const QString &desktopId);
void syncBankProfilesFromServer();
void syncBanksIfEmpty();
};

View File

@ -49,14 +49,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
auto *bankListAction = new QAction("Банки", this);
auto *tutorialPageAction = new QAction("Инструкция", this);
auto *eventAction = new QAction("События", this);
auto *errorLogAction = new QAction("Ошибки", this);
auto *fileLogAction = new QAction("Логи", this);
menu->addAction(devicesPageAction);
// menu->addAction(accountPageAction);
menu->addAction(bankListAction);
menu->addAction(eventAction);
menu->addAction(tutorialPageAction);
menu->addAction(errorLogAction);
menu->addAction(fileLogAction);
connect(devicesPageAction, &QAction::triggered, this, [=]() {
@ -98,15 +96,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
m_eventWindow->activateWindow();
});
connect(errorLogAction, &QAction::triggered, this, [=]() {
if (!m_logWindow) {
m_logWindow = new LogWindow(this);
}
m_logWindow->show();
m_logWindow->raise();
m_logWindow->activateWindow();
});
connect(fileLogAction, &QAction::triggered, this, [=]() {
if (!m_fileLogWindow) {
m_fileLogWindow = new FileLogWindow(this);

View File

@ -32,7 +32,6 @@ private:
QWidget *m_accountsWindow = nullptr;
QWidget *m_tutorialWindow = nullptr;
QWidget *m_bankListWindow = nullptr;
QWidget *m_logWindow = nullptr;
QWidget *m_fileLogWindow = nullptr;
QWidget *m_eventWindow = nullptr;

View File

@ -19,15 +19,15 @@
#include "black/LoginAndCheckAccountsScript.h"
#include "black/GetProfileInfoScript.h"
#include "black/GetCardInfoScript.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "bank/TransactionWindow.h"
#include "device/EditBankAccountDialog.h"
#include "device/EditBankDialog.h"
#include "net/NetworkService.h"
AccountSettingsWindow::AccountSettingsWindow(
QWidget *parent, QString deviceId, QString deviceName, const ApplicationInfo &app
QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app
) : QWidget(parent,
Qt::Window
| Qt::WindowTitleHint
@ -90,7 +90,7 @@ AccountSettingsWindow::AccountSettingsWindow(
const QString comment = dlg.secondValue();
bool on = dlg.toggled();
if (!ApplicationDAO::update(m_applicationInfo.id, pinCode, comment, on ? "active" : "off")) {
if (!BankProfileDAO::update(m_applicationInfo.id, pinCode, comment, on ? "active" : "off")) {
qDebug() << "Cant update bank data";
} else {
m_applicationInfo.pinCode = pinCode;
@ -132,6 +132,17 @@ AccountSettingsWindow::AccountSettingsWindow(
m_profileLabel->setTextFormat(Qt::RichText);
m_profileLabel->setStyleSheet("margin-bottom: 8px;");
profileRow->addWidget(m_profileLabel);
// Статус синхронизации с сервером
const QString syncText = m_applicationInfo.bankProfileId.isEmpty()
? "<span style='color:red;'>&#9679; Не синхронизирован</span>"
: "<span style='color:green;'>&#9679; Синхронизирован</span>";
auto *syncLabel = new QLabel(syncText);
syncLabel->setTextFormat(Qt::RichText);
syncLabel->setStyleSheet("margin-bottom: 8px; margin-left: 16px;");
profileRow->addWidget(syncLabel);
profileRow->addStretch();
layout->addLayout(profileRow);
QPushButton *soloButton = new QPushButton("Запустить проверку пин-кода");
@ -320,35 +331,6 @@ AccountSettingsWindow::AccountSettingsWindow(
layout->addWidget(tableWidget);
QPushButton *addAccountButton = new QPushButton("Добавить аккаунт");
addAccountButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
AccountInfo account;
connect(addAccountButton, &QPushButton::clicked, this, [this, tableWidget, account]() {
EditBankAccountDialog dlg(account, this);
if (dlg.exec() == QDialog::Accepted) {
const QString cardNumber = dlg.cardNumberValue();
const QString currency = dlg.currencyValue();
bool on = dlg.toggled();
AccountInfo accountInfo;
accountInfo.currency = currency;
accountInfo.description = cardNumber;
accountInfo.amount = 0;
accountInfo.appCode = m_applicationInfo.code;
accountInfo.deviceId = m_deviceId;
accountInfo.status = on ? AccountStatus::Active : AccountStatus::Off;
accountInfo.lastNumbers = cardNumber.right(4);
if (!AccountDAO::upsertAccount(accountInfo)) {
qDebug() << "Cant insert bank account data";
} else {
reloadContent(tableWidget);
tableWidget->resizeColumnsToContents();
updateAccountData(accountInfo);
}
}
});
layout->addWidget(addAccountButton);
tableWidget->resizeRowsToContents();
@ -365,11 +347,22 @@ AccountSettingsWindow::AccountSettingsWindow(
void AccountSettingsWindow::startCheckPinCode(const QString &appCode) {
QThread *thread = new QThread(nullptr);
const QString deviceId = m_deviceId;
auto onResult = [deviceId, appCode](const QString &result) {
if (result.isEmpty()) {
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
if (app.id >= 0) {
BankProfileDAO::updatePinCodeStatus(app.id, "yes", "");
}
}
};
if (appCode == "ozon") {
auto *worker = new Ozon::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thread, &QThread::quit);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
@ -377,6 +370,7 @@ void AccountSettingsWindow::startCheckPinCode(const QString &appCode) {
auto *worker = new Black::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start);
connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult);
connect(worker, &Black::LoginAndCheckAccountsScript::finished, thread, &QThread::quit);
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
@ -438,7 +432,7 @@ void AccountSettingsWindow::startGetCardInfo(const QString &appCode) {
void AccountSettingsWindow::startGetLastTransactions(const QString &appCode, int maxCount) {
if (appCode == "ozon") {
QThread *thread = new QThread(nullptr);
auto *worker = new Ozon::GetLastTransactionsScript(m_deviceId, appCode, maxCount, nullptr);
auto *worker = new Ozon::GetLastTransactionsScript(m_deviceId, appCode, maxCount);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Ozon::GetLastTransactionsScript::start);
connect(worker, &Ozon::GetLastTransactionsScript::finished, thread, &QThread::quit);
@ -458,7 +452,7 @@ void AccountSettingsWindow::startGetLastTransactions(const QString &appCode, int
}
void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) {
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code);
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code);
if (app.bankProfileId.isEmpty()) {
qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << code;
return;
@ -477,7 +471,7 @@ void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString
thread->start();
}
void AccountSettingsWindow::updateAccountData(const AccountInfo &account) {
void AccountSettingsWindow::updateAccountData(const MaterialInfo &account) {
QJsonArray list;
QJsonObject obj;
obj["appName"] = account.appCode;
@ -485,7 +479,7 @@ void AccountSettingsWindow::updateAccountData(const AccountInfo &account) {
obj["amount"] = account.amount;
obj["description"] = account.description;
obj["currency"] = account.currency;
obj["status"] = accountStatusToString(account.status);
obj["status"] = materialStatusToString(account.status);
list.append(obj);
@ -502,11 +496,11 @@ void AccountSettingsWindow::updateAccountData(const AccountInfo &account) {
}
void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
QList<AccountInfo> accounts = AccountDAO::getAccounts(m_deviceId, m_applicationInfo.code);
QList<MaterialInfo> accounts = MaterialDAO::getAccounts(m_deviceId, m_applicationInfo.code);
tableWidget->clearContents();
tableWidget->setRowCount(accounts.length());
tableWidget->setColumnCount(8);
tableWidget->setColumnCount(9);
tableWidget->setHorizontalHeaderLabels({
"Время обновления",
"Номер",
@ -514,6 +508,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
"Сумма",
"Валюта",
"Статус",
"Сервер",
"",
""
});
@ -526,7 +521,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
for (int k = 0; k < accounts.size(); ++k) {
const AccountInfo &account = accounts[k];
const MaterialInfo &account = accounts[k];
QTableWidgetItem *time = new QTableWidgetItem(account.updateTime.toString("dd.MM.yyyy HH:mm:ss"));
time->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
@ -554,18 +549,24 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
currency->setFlags(currency->flags() & ~Qt::ItemIsEditable);
tableWidget->setItem(k, 4, currency);
QTableWidgetItem *status = new QTableWidgetItem(accountStatusToUiString(account.status));
QTableWidgetItem *status = new QTableWidgetItem(materialStatusToUiString(account.status));
status->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
status->setFlags(status->flags() & ~Qt::ItemIsEditable);
if (account.status != AccountStatus::Active) {
if (account.status != MaterialStatus::Active) {
status->setForeground(QBrush(Qt::red));
}
tableWidget->setItem(k, 5, status);
auto *syncItem = new QTableWidgetItem(account.materialId.isEmpty() ? "Не синхр." : "Синхр.");
syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
syncItem->setForeground(account.materialId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen));
syncItem->setFlags(syncItem->flags() & ~Qt::ItemIsEditable);
tableWidget->setItem(k, 6, syncItem);
auto *deleteBankAccountBtn = new QPushButton("Удалить", tableWidget);
deleteBankAccountBtn->setStyleSheet("background-color: red; color: white;");
tableWidget->setCellWidget(k, 6, deleteBankAccountBtn);
tableWidget->setCellWidget(k, 7, deleteBankAccountBtn);
connect(deleteBankAccountBtn, &QPushButton::clicked, this, [this, tableWidget, account]() {
QMessageBox msgBox(this);
@ -576,7 +577,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
msgBox.setDefaultButton(cancelBtn);
msgBox.exec();
if (msgBox.clickedButton() == deleteBtn) {
if (AccountDAO::deleteAccount(account.id)) {
if (MaterialDAO::deleteAccount(account.id)) {
reloadContent(tableWidget);
tableWidget->resizeColumnsToContents();
} else {
@ -587,7 +588,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
auto *transactionsBtn = new QPushButton("Транзакции", tableWidget);
transactionsBtn->setStyleSheet("background-color: #1565C0; color: white;");
tableWidget->setCellWidget(k, 7, transactionsBtn);
tableWidget->setCellWidget(k, 8, transactionsBtn);
connect(transactionsBtn, &QPushButton::clicked, this, [this, account]() {
auto *txWindow = new TransactionWindow(account.id, this);

View File

@ -3,19 +3,19 @@
#include <QTableWidget>
#include <QWidget>
#include "db/AccountInfo.h"
#include "db/ApplicationInfo.h"
#include "db/MaterialInfo.h"
#include "db/BankProfileInfo.h"
class AccountSettingsWindow final : public QWidget {
Q_OBJECT
public:
AccountSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const ApplicationInfo &app);
AccountSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app);
private:
QString m_deviceId;
QString m_deviceName;
ApplicationInfo m_applicationInfo;
BankProfileInfo m_applicationInfo;
QLabel *m_statusLabel;
QLabel *m_commentLabel;
@ -33,5 +33,5 @@ private:
void sendAppStatus(const QString &deviceId, const QString &code, const QString &status);
void updateAccountData(const AccountInfo &account);
void updateAccountData(const MaterialInfo &account);
};

View File

@ -7,7 +7,7 @@
#include <QScrollArea>
#include "TransactionWindow.h"
#include "dao/AccountDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/DeviceDAO.h"
AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) {
@ -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, "ozon");
QList<MaterialInfo> accounts = MaterialDAO::getAccounts(device.id, "ozon");
// Текст перед первой таблицей
QString title = QString("%1 [%2] | %3").arg(device.name, device.id, device.status);
QLabel *label = new QLabel(title, this);
@ -54,14 +54,14 @@ AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) {
for (int k = 0; k < accounts.size(); ++k) {
AccountInfo account = accounts[k];
MaterialInfo account = accounts[k];
QTableWidgetItem *item = new QTableWidgetItem("*" + account.lastNumbers);
item->setData(Qt::UserRole, account.id);
tableWidget->setItem(k, 0, item);
tableWidget->setItem(k, 1, new QTableWidgetItem(QString::number(account.amount)));
tableWidget->setItem(k, 2, new QTableWidgetItem(account.updateTime.toString("dd.MM.yyyy HH:mm:ss")));
tableWidget->setItem(k, 3, new QTableWidgetItem(account.description));
tableWidget->setItem(k, 4, new QTableWidgetItem(accountStatusToString(account.status)));
tableWidget->setItem(k, 4, new QTableWidgetItem(materialStatusToString(account.status)));
totalHeight += tableWidget->rowHeight(k);
}
tableWidget->setFixedHeight(totalHeight + 2);

View File

@ -15,8 +15,9 @@
#include <QJsonObject>
#include <QThread>
#include "dao/ApplicationDAO.h"
#include "db/ApplicationInfo.h"
#include "dao/BankProfileDAO.h"
#include "dao/EventDAO.h"
#include "db/BankProfileInfo.h"
#include "net/NetworkService.h"
#include "common/PaddedItemDelegate.h"
#include "device/EditBankDialog.h"
@ -29,7 +30,7 @@ QTableWidgetItem *BankSettingsWindow::createTableWidget(const QString &text) {
return item;
}
QString parseStatus(const ApplicationInfo &app) {
QString parseStatus(const BankProfileInfo &app) {
if (!app.install) {
return "не установлен";
}
@ -47,11 +48,22 @@ QString parseStatus(const ApplicationInfo &app) {
void BankSettingsWindow::startCheckPinCode(const QString &appCode) {
QThread *thread = new QThread(nullptr);
const QString deviceId = m_deviceId;
auto onResult = [deviceId, appCode](const QString &result) {
if (result.isEmpty()) {
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
if (app.id >= 0) {
BankProfileDAO::updatePinCodeStatus(app.id, "yes", "");
}
}
};
if (appCode == "ozon") {
auto *worker = new Ozon::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thread, &QThread::quit);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
@ -59,6 +71,7 @@ void BankSettingsWindow::startCheckPinCode(const QString &appCode) {
auto *worker = new Black::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start);
connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult);
connect(worker, &Black::LoginAndCheckAccountsScript::finished, thread, &QThread::quit);
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
@ -70,13 +83,45 @@ void BankSettingsWindow::startCheckPinCode(const QString &appCode) {
thread->start();
}
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName) {
QJsonObject errData;
errData["status"] = "error";
errData["description"] = "Bank profile disabled";
QJsonObject obj;
obj["type"] = type;
obj["material_id"] = materialId;
obj["bank_name"] = bankName;
obj["data"] = errData;
auto *thread = new QThread;
auto *net = new NetworkService;
net->moveToThread(thread);
net->setPayload(QJsonDocument(obj).toJson());
QObject::connect(thread, &QThread::started, net, &NetworkService::postTaskResult);
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 sendAppStatus(const QString &deviceId, const QString &code, const QString &status) {
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code);
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code);
if (app.bankProfileId.isEmpty()) {
qWarning() << "[BankSettings] sendAppStatus: no bankProfileId for" << code;
return;
}
// При выключении — отменяем ожидающие задачи
if (status != "active") {
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByBank(deviceId, code);
for (const EventInfo &e : cancelled) {
sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName);
EventDAO::markSentToServer(e.id);
qDebug() << "[BankSettings] cancelled task" << e.id << eventTypeToString(e.type);
}
}
const bool enable = (status == "active");
auto *thread = new QThread;
auto *net = new NetworkService;
@ -91,15 +136,16 @@ void sendAppStatus(const QString &deviceId, const QString &code, const QString &
}
void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
QList<ApplicationInfo> applications = ApplicationDAO::getApplicationsByDeviceId(m_deviceId);
QList<BankProfileInfo> applications = BankProfileDAO::getApplicationsByDeviceId(m_deviceId);
tableWidget->setRowCount(applications.length());
tableWidget->setColumnCount(7);
tableWidget->setColumnCount(8);
tableWidget->setHorizontalHeaderLabels({
"",
"Имя",
"Пин-код",
"Статус",
"Сервер",
"Комментарий",
"",
""
@ -109,17 +155,18 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
tableWidget->setTextElideMode(Qt::ElideNone);
tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
for (int i = 0; i < 7; ++i) {
for (int i = 0; i < 8; ++i) {
tableWidget->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
}
for (int i = 0; i < applications.length(); ++i) {
tableWidget->verticalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
}
tableWidget->horizontalHeader()->setSectionResizeMode(4, QHeaderView::Stretch);
tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch);
tableWidget->setColumnWidth(4, 140);
for (int k = 0; k < applications.size(); ++k) {
const ApplicationInfo &app = applications[k];
const BankProfileInfo &app = applications[k];
auto *icon = new QLabel(tableWidget);
@ -151,20 +198,23 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
tableWidget->setItem(k, 3, createTableWidget(parseStatus(app)));
// tableWidget->setItem(k, 4, createTableWidget(app.comment));
// Статус синхронизации с сервером
auto *syncItem = new QTableWidgetItem(app.bankProfileId.isEmpty() ? "Не синхронизирован" : "Синхронизирован");
syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
syncItem->setForeground(app.bankProfileId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen));
tableWidget->setItem(k, 4, syncItem);
QTableWidgetItem *item = new QTableWidgetItem(app.comment);
item->setTextAlignment(Qt::AlignLeft | Qt::AlignTop);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
// item->setSizeHint(QSize(100, app.comment.length() / 2)); // Минимальная высота ячейки для переноса
tableWidget->setItem(k, 4, item);
tableWidget->setItem(k, 5, item);
if (app.install) {
if (!app.pinCode.isEmpty() && !app.pinCodeChecked) {
auto *pinCodeBtn = new QPushButton(" Проверить пин-код ", tableWidget);
pinCodeBtn->setProperty("btnClass", "pincode");
tableWidget->setCellWidget(k, 5, pinCodeBtn);
tableWidget->setCellWidget(k, 6, pinCodeBtn);
connect(pinCodeBtn, &QPushButton::clicked, this, [this, app]() {
QMessageBox msgBox(this);
@ -187,7 +237,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
deleteBtn->setProperty("btnClass", "delete");
// deleteBtn->setStyleSheet("padding: 4px 8px;");
// deleteBtn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
tableWidget->setCellWidget(k, 5, deleteBtn);
tableWidget->setCellWidget(k, 6, deleteBtn);
connect(deleteBtn, &QPushButton::clicked, this, [this,tableWidget, app]() {
QMessageBox msgBox(this);
@ -201,7 +251,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
msgBox.exec();
if (msgBox.clickedButton() == deleteBtn) {
if (!ApplicationDAO::update(app.id, app.pinCode, app.comment, "off")) {
if (!BankProfileDAO::update(app.id, app.pinCode, app.comment, "off")) {
qDebug() << "Cant update bank data";
} else {
sendAppStatus(app.deviceId, app.code, "off");
@ -222,7 +272,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
auto *btn = new QPushButton(" Редактировать ", tableWidget);
btn->setProperty("btnClass", "edit");
// btn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
tableWidget->setCellWidget(k, 6, btn);
tableWidget->setCellWidget(k, 7, btn);
connect(btn, &QPushButton::clicked, this, [this, tableWidget, k,app]() {
BankEditDialog dlg(app, this);
if (dlg.exec() == QDialog::Accepted) {
@ -230,7 +280,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
const QString comment = dlg.secondValue();
bool on = dlg.toggled();
if (!ApplicationDAO::update(app.id, pinCode, comment, on ? "active" : "off")) {
if (!BankProfileDAO::update(app.id, pinCode, comment, on ? "active" : "off")) {
qDebug() << "Cant update bank data";
} else {
sendAppStatus(app.deviceId, app.code, on ? "active" : "off");
@ -246,7 +296,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
}
}
BankSettingsWindow::BankSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const ApplicationInfo &app)
BankSettingsWindow::BankSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app)
: QWidget(parent,
Qt::Window
| Qt::WindowTitleHint

View File

@ -1,13 +1,13 @@
#pragma once
#include <qtablewidget.h>
#include <QWidget>
#include "db/ApplicationInfo.h"
#include "db/BankProfileInfo.h"
class BankSettingsWindow final : public QWidget {
Q_OBJECT
public:
BankSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const ApplicationInfo &app);
BankSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app);
private:
QString m_deviceId;

View File

@ -11,7 +11,7 @@
#include <QTableWidget>
#include <QStyledItemDelegate>
#include "dao/AccountDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/TransactionDAO.h"
class AmountDelegate : public QStyledItemDelegate {
@ -67,7 +67,7 @@ TransactionWindow::TransactionWindow(const int accountId, QWidget *parent): QDia
scrollArea->setStyleSheet("border: none;"); // Убираем рамку
layout->setAlignment(Qt::AlignTop);
AccountInfo account = AccountDAO::getAccountById(accountId);
MaterialInfo account = MaterialDAO::getAccountById(accountId);
QList<TransactionInfo> transactions = TransactionDAO::getTransactions(accountId);
QTableWidget *tableWidget = new QTableWidget(this);

View File

@ -7,11 +7,12 @@
#include <QPushButton>
#include <QLabel>
#include <QSettings>
#include "bank/AccountSettingsWindow.h"
#include "dao/ApplicationDAO.h"
#include "db/ApplicationInfo.h"
#include "dao/BankProfileDAO.h"
#include "db/BankProfileInfo.h"
static QString formatStatus(const ApplicationInfo &app) {
static QString formatStatus(const BankProfileInfo &app) {
if (!app.install) return "не установлен";
if (app.status == "active") {
if (!app.pinCodeChecked)
@ -63,7 +64,29 @@ DeviceSettingsWindow::DeviceSettingsWindow(QWidget *parent, const QString &devic
}
void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) {
QList<ApplicationInfo> apps = ApplicationDAO::getApplicationsByDeviceId(m_deviceId);
QList<BankProfileInfo> apps = BankProfileDAO::getAllApplicationsByDeviceId(m_deviceId);
// Дополняем список банками из config.ini, которых нет в БД
const QSettings configSettings("config.ini", QSettings::IniFormat);
const QStringList banks = configSettings.value("apps/list").toStringList();
QSet<QString> existingCodes;
for (const BankProfileInfo &a : apps) {
existingCodes.insert(a.code);
}
for (const QString &rawBank : banks) {
const QString bank = rawBank.trimmed();
if (bank.isEmpty() || existingCodes.contains(bank)) continue;
BankProfileInfo stub;
stub.id = -1;
stub.deviceId = m_deviceId;
stub.code = bank;
stub.name = configSettings.value(bank + "/name", bank).toString();
stub.package = configSettings.value(bank + "/android_package_name").toString();
stub.currency = configSettings.value(bank + "/currency").toString();
stub.status = "off";
stub.install = false;
apps.append(stub);
}
tableWidget->clearContents();
tableWidget->setRowCount(apps.size());
@ -76,7 +99,7 @@ void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) {
tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch);
for (int k = 0; k < apps.size(); ++k) {
const ApplicationInfo &app = apps[k];
const BankProfileInfo &app = apps[k];
// Иконка
auto *icon = new QLabel(tableWidget);

View File

@ -5,6 +5,8 @@
#include <QImage>
#include <QPushButton>
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPointer>
#include <QPropertyAnimation>
#include <QGraphicsScene>
@ -13,14 +15,15 @@
#include "bank/AccountSettingsWindow.h"
#include "bank/BankSettingsWindow.h"
#include "DeviceSettingsWindow.h"
#include "dao/ApplicationDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "db/ApplicationInfo.h"
#include "dao/EventDAO.h"
#include "db/BankProfileInfo.h"
#include "net/NetworkService.h"
#include <QThread>
QString getColorByStatus(const ApplicationInfo &app) {
QString getColorByStatus(const BankProfileInfo &app) {
if (!app.install) {
return "black";
}
@ -36,13 +39,44 @@ QString getColorByStatus(const ApplicationInfo &app) {
}
}
static void sendTaskErrorToServer(const QString &type, const QString &materialId, const QString &bankName) {
QJsonObject errData;
errData["status"] = "error";
errData["description"] = "Bank profile disabled";
QJsonObject obj;
obj["type"] = type;
obj["material_id"] = materialId;
obj["bank_name"] = bankName;
obj["data"] = errData;
auto *thread = new QThread;
auto *net = new NetworkService;
net->moveToThread(thread);
net->setPayload(QJsonDocument(obj).toJson());
QObject::connect(thread, &QThread::started, net, &NetworkService::postTaskResult);
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 sendBankStatus(const QString &deviceId, const QString &code, const QString &status) {
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, code);
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code);
if (app.bankProfileId.isEmpty()) {
qWarning() << "[DeviceWidget] sendBankStatus: no bankProfileId for" << code;
return;
}
// При выключении — отменяем ожидающие задачи
if (status != "active") {
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByBank(deviceId, code);
for (const EventInfo &e : cancelled) {
sendTaskErrorToServer(eventTypeToString(e.type), e.externalId, e.bankName);
EventDAO::markSentToServer(e.id);
}
}
const bool enable = (status == "active");
auto *thread = new QThread;
auto *net = new NetworkService;
@ -56,7 +90,7 @@ void sendBankStatus(const QString &deviceId, const QString &code, const QString
thread->start();
}
void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline) {
void DeviceWidget::buildBankRow(const BankProfileInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline) {
rowWidget->setContentsMargins(0, 0, 0, 0);
rowWidget->setStyleSheet(
"background-color: rgba(0, 0, 0, 0.6);" // полупрозрачный фон
@ -78,7 +112,16 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget,
);
auto *label = new QLabel(app.name, parent);
QString labelText;
if (!app.fullName.isEmpty()) {
labelText = app.fullName;
if (!app.phone.isEmpty()) {
labelText += "\n" + app.phone;
}
} else {
labelText = app.name;
}
auto *label = new QLabel(labelText, parent);
label->setStyleSheet(
"background-color: transparent;"
"margin: 0px;"
@ -129,7 +172,7 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget,
msgBox.exec();
if (msgBox.clickedButton() == confirmBtn) {
if (!ApplicationDAO::update(app.id, app.pinCode, app.comment, newStatus)) {
if (!BankProfileDAO::update(app.id, app.pinCode, app.comment, newStatus)) {
qDebug() << "Cant update bank data";
} else {
sendBankStatus(app.deviceId, app.code, newStatus);
@ -175,7 +218,7 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget,
// hbox->addStretch();
}
void DeviceWidget::setOpenBanksSettings(QPushButton *button, const ApplicationInfo &app) {
void DeviceWidget::setOpenBanksSettings(QPushButton *button, const BankProfileInfo &app) {
QString deviceId = m_device.id;
QString deviceName = m_device.name;
connect(button, &QPushButton::clicked, this, [deviceId, deviceName, app]() {
@ -219,6 +262,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
statusLabel->setStyleSheet(
"background-color: rgba(0, 0, 0, 0.5);"
"color: white;"
"font-size: 10px;"
);
layout->addWidget(statusLabel, 0, 0, Qt::AlignTop | Qt::AlignLeft);
@ -274,7 +318,15 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
msgBox.exec();
if (msgBox.clickedButton() == confirmBtn) {
DeviceDAO::deleteDevice(deviceId);
auto *thread = new QThread;
auto *net = new NetworkService;
net->setDeviceId(deviceId);
net->moveToThread(thread);
QObject::connect(thread, &QThread::started, net, &NetworkService::deleteDeviceFromServer);
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();
}
});
@ -307,8 +359,8 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
const bool isOffline = device.status != "device";
QList<ApplicationInfo> apps = ApplicationDAO::getApplicationsByDeviceId(device.id);
for (ApplicationInfo &app: apps) {
QList<BankProfileInfo> apps = BankProfileDAO::getApplicationsByDeviceId(device.id);
for (BankProfileInfo &app: apps) {
if (app.install) {
auto *rowWidget = new QWidget(appsView);
buildBankRow(app, rowWidget, appsView, isOffline);

View File

@ -3,7 +3,7 @@
#include <qtablewidget.h>
#include <QWidget>
#include "db/ApplicationInfo.h"
#include "db/BankProfileInfo.h"
#include "db/DeviceInfo.h"
class DeviceWidget final : public QWidget {
@ -15,9 +15,9 @@ public:
private:
const DeviceInfo m_device;
void buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline = false);
void buildBankRow(const BankProfileInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline = false);
void setOpenBanksSettings(QPushButton *button, const ApplicationInfo &app);
void setOpenBanksSettings(QPushButton *button, const BankProfileInfo &app);
signals:
void deleteRequested(const QString &deviceId);

View File

@ -7,13 +7,13 @@
#include <QComboBox>
#include <QSettings>
#include "db/AccountInfo.h"
#include "db/MaterialInfo.h"
class EditBankAccountDialog final : public QDialog {
Q_OBJECT
public:
explicit EditBankAccountDialog(const AccountInfo &account, QWidget *parent = nullptr) : QDialog(parent) {
explicit EditBankAccountDialog(const MaterialInfo &account, QWidget *parent = nullptr) : QDialog(parent) {
if (account.id > 0) {
setWindowTitle("Настройка аккаунта *" + account.lastNumbers);
} else {
@ -36,7 +36,7 @@ public:
currency->setCurrentText(account.currency);
toggle = new QCheckBox(tr("В работе"), this);
toggle->setChecked(account.status == AccountStatus::Active);
toggle->setChecked(account.status == MaterialStatus::Active);
form->addRow(tr("Номер карты:"), cardNumber);
form->addRow(tr("Валюта:"), currency);

View File

@ -5,13 +5,13 @@
#include <QCheckBox>
#include <QDialogButtonBox>
#include <qtextedit.h>
#include "db/ApplicationInfo.h"
#include "db/BankProfileInfo.h"
class BankEditDialog final : public QDialog {
Q_OBJECT
public:
explicit BankEditDialog(const ApplicationInfo &app, QWidget *parent = nullptr) : QDialog(parent) {
explicit BankEditDialog(const BankProfileInfo &app, QWidget *parent = nullptr) : QDialog(parent) {
setWindowTitle("Настройка " + app.name);
setModal(true);

View File

@ -67,7 +67,7 @@ static QString typeDisplayName(EventType t) {
EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
setWindowTitle("События");
resize(1400, 600);
resize(1400, 700);
// ── Таблица ───────────────────────────────────────────────────────────────
m_table = new QTableWidget(this);
@ -91,6 +91,19 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
m_table->setColumnWidth(COL_SENT, 45);
m_table->setColumnWidth(COL_ACTION, 80);
// ── Панель скриншота ──────────────────────────────────────────────────────
m_screenshotLabel = new QLabel(this);
m_screenshotLabel->setAlignment(Qt::AlignCenter);
m_screenshotLabel->setMinimumWidth(300);
m_screenshotLabel->setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;");
m_screenshotLabel->setText("Выберите событие\nдля просмотра скриншота");
auto *splitter = new QSplitter(Qt::Horizontal, this);
splitter->addWidget(m_table);
splitter->addWidget(m_screenshotLabel);
splitter->setStretchFactor(0, 3);
splitter->setStretchFactor(1, 1);
// ── Пагинация ─────────────────────────────────────────────────────────────
m_prevBtn = new QPushButton("", this);
m_nextBtn = new QPushButton("", this);
@ -108,7 +121,7 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
paginationLayout->addWidget(refreshBtn);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(m_table, 1);
mainLayout->addWidget(splitter, 1);
mainLayout->addLayout(paginationLayout);
// ── Сигналы ───────────────────────────────────────────────────────────────
@ -122,6 +135,11 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
m_page = 0;
loadPage();
});
connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) {
if (row >= 0 && row < m_eventIds.size()) {
showScreenshot(m_eventIds[row]);
}
});
connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int col) {
if (col != COL_JSON) return;
auto *item = m_table->item(row, col);
@ -150,9 +168,11 @@ void EventWindow::loadPage() {
m_page = qBound(0, m_page, m_totalPages - 1);
const QList<EventInfo> events = EventDAO::getEvents(m_page, m_pageSize);
m_eventIds.clear();
m_table->setRowCount(static_cast<int>(events.size()));
for (int i = 0; i < events.size(); ++i) {
m_eventIds.append(events[i].id);
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(
@ -204,6 +224,24 @@ void EventWindow::loadPage() {
updatePagination();
}
void EventWindow::showScreenshot(int eventId) {
const QByteArray data = EventDAO::getScreenshot(eventId);
if (data.isEmpty()) {
m_screenshotLabel->setText("Нет скриншота");
return;
}
QPixmap pixmap;
pixmap.loadFromData(data);
if (pixmap.isNull()) {
m_screenshotLabel->setText("Ошибка загрузки");
return;
}
m_screenshotLabel->setPixmap(pixmap.scaled(
m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
void EventWindow::updatePagination() {
m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages));
m_prevBtn->setEnabled(m_page > 0);

View File

@ -2,6 +2,7 @@
#include <QDialog>
#include <QLabel>
#include <QPushButton>
#include <QSplitter>
#include <QTableWidget>
class EventWindow final : public QDialog {
@ -13,6 +14,7 @@ public:
private:
QTableWidget *m_table;
QLabel *m_pageLabel;
QLabel *m_screenshotLabel;
QPushButton *m_prevBtn;
QPushButton *m_nextBtn;
@ -20,6 +22,10 @@ private:
int m_pageSize = 50;
int m_totalPages = 1;
// Кешируем event id текущей страницы для загрузки скриншота по клику
QList<int> m_eventIds;
void loadPage();
void updatePagination();
void showScreenshot(int eventId);
};

View File

@ -1,10 +1,21 @@
#include "FileLogWindow.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QHttpMultiPart>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QProcess>
#include <QTemporaryFile>
#include <QVBoxLayout>
#include "dao/EventDAO.h"
#include "dao/GeneralLogDAO.h"
#include "dao/SettingsDAO.h"
static constexpr int COL_ID = 0;
static constexpr int COL_TIME = 1;
@ -38,6 +49,8 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
m_pageLabel->setMinimumWidth(140);
auto *refreshBtn = new QPushButton("Обновить", this);
m_sendBtn = new QPushButton("Отправить", this);
m_sendBtn->setStyleSheet("background-color: #0088cc; color: white; border-radius: 4px; padding: 4px 12px;");
auto *paginationLayout = new QHBoxLayout;
paginationLayout->addWidget(m_prevBtn);
@ -46,6 +59,7 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
paginationLayout->addStretch();
paginationLayout->addWidget(m_showAllCheck);
paginationLayout->addWidget(refreshBtn);
paginationLayout->addWidget(m_sendBtn);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(m_table, 1);
@ -65,6 +79,7 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
m_page = 0;
loadPage();
});
connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogToTelegram);
loadPage();
}
@ -102,3 +117,141 @@ void FileLogWindow::updatePagination() {
m_prevBtn->setEnabled(m_page > 0);
m_nextBtn->setEnabled(m_page < m_totalPages - 1);
}
void FileLogWindow::sendLogToTelegram() {
const QString logPath = QCoreApplication::applicationDirPath() + "/application.log";
if (!QFile::exists(logPath)) {
QMessageBox::warning(this, "Ошибка", "Файл application.log не найден");
return;
}
m_sendBtn->setEnabled(false);
m_sendBtn->setText("Подготовка...");
// Собираем временную папку для архива
const QString tmpDir = QDir::tempPath() + "/arc_logs_export";
QDir().mkpath(tmpDir);
// 1. Копируем application.log
QFile::remove(tmpDir + "/application.log");
QFile::copy(logPath, tmpDir + "/application.log");
// 2. Экспортируем events с ошибками в errors.txt
const QList<EventInfo> errors = EventDAO::getAllEvents(EventStatus::Error);
if (!errors.isEmpty()) {
QFile errFile(tmpDir + "/errors.txt");
if (errFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&errFile);
for (const EventInfo &e : errors) {
out << "--- Event #" << e.id << " ---\n"
<< "Type: " << eventTypeToString(e.type) << "\n"
<< "Bank: " << e.bankName << "\n"
<< "Device: " << e.deviceId << "\n"
<< "Amount: " << e.amount << "\n"
<< "Phone: " << e.phone << "\n"
<< "Comment: " << e.comment << "\n"
<< "Time: " << e.timestamp.toString(Qt::ISODate) << "\n"
<< "JSON: " << e.json << "\n\n";
}
errFile.close();
}
}
// 3. Экспортируем скриншоты ошибок из events
QDir().mkpath(tmpDir + "/screenshots");
int screenshotCount = 0;
for (const EventInfo &e : errors) {
const QByteArray screenshot = EventDAO::getScreenshot(e.id);
if (screenshot.isEmpty()) continue;
const QString fileName = QString("screenshots/%1_%2_%3.png")
.arg(e.id)
.arg(eventTypeToString(e.type))
.arg(e.timestamp.toString("yyyyMMdd_HHmmss"));
QFile imgFile(tmpDir + "/" + fileName);
if (imgFile.open(QIODevice::WriteOnly)) {
imgFile.write(screenshot);
imgFile.close();
++screenshotCount;
}
}
// 4. Создаём ZIP
const QString zipPath = QDir::tempPath() + "/arc_logs.zip";
QFile::remove(zipPath);
QProcess zip;
zip.setWorkingDirectory(tmpDir);
zip.start("zip", QStringList() << "-r" << zipPath << ".");
zip.waitForFinished(30000);
// Чистим временную папку
QDir(tmpDir).removeRecursively();
if (zip.exitCode() != 0 || !QFile::exists(zipPath)) {
QMessageBox::warning(this, "Ошибка", "Не удалось создать архив");
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
m_sendBtn->setText("Отправка...");
QFile *zipFile = new QFile(zipPath);
if (!zipFile->open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив");
delete zipFile;
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
return;
}
// Telegram Bot API
const QString botToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8";
const QString chatId = "-5177086220";
const QString desktopId = SettingsDAO::get("desktop_id");
const QString caption = "Logs from desktop: " + desktopId
+ "\nErrors: " + QString::number(errors.size())
+ "\nScreenshots: " + QString::number(screenshotCount);
const QUrl url("https://api.telegram.org/bot" + botToken + "/sendDocument");
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart chatPart;
chatPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="chat_id")");
chatPart.setBody(chatId.toUtf8());
multiPart->append(chatPart);
QHttpPart captionPart;
captionPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="caption")");
captionPart.setBody(caption.toUtf8());
multiPart->append(captionPart);
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
R"(form-data; name="document"; filename="arc_logs.zip")");
filePart.setBodyDevice(zipFile);
zipFile->setParent(multiPart);
multiPart->append(filePart);
auto *manager = new QNetworkAccessManager(this);
QNetworkReply *reply = manager->post(QNetworkRequest(url), multiPart);
multiPart->setParent(reply);
connect(reply, &QNetworkReply::finished, this, [this, reply, manager, zipPath]() {
reply->deleteLater();
manager->deleteLater();
QFile::remove(zipPath);
m_sendBtn->setEnabled(true);
m_sendBtn->setText("Отправить");
if (reply->error() != QNetworkReply::NoError) {
QMessageBox::warning(this, "Ошибка", "Не удалось отправить: " + reply->errorString());
return;
}
QMessageBox::information(this, "Готово", "Логи отправлены в Telegram");
});
}

View File

@ -16,6 +16,7 @@ private:
QLabel *m_pageLabel;
QPushButton *m_prevBtn;
QPushButton *m_nextBtn;
QPushButton *m_sendBtn;
QCheckBox *m_showAllCheck;
int m_page = 0;
@ -25,4 +26,5 @@ private:
QStringList currentLevelFilter() const;
void loadPage();
void updatePagination();
void sendLogToTelegram();
};