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

View File

@ -34,12 +34,12 @@ protected:
int &counter, int width, int height 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( void updateTransactionData(
const AccountInfo &account, const MaterialInfo &account,
int transactionId, int transactionId,
TransactionStatus status, TransactionStatus status,
const QString &oldDesc, const QString &oldDesc,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,12 +27,12 @@ protected:
bool findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers); 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( void updateTransactionData(
const AccountInfo &account, const MaterialInfo &account,
int transactionId, int transactionId,
TransactionStatus status, TransactionStatus status,
const QString &oldDesc, const QString &oldDesc,

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
#pragma once #pragma once
#include "CommonScript.h" #include "CommonScript.h"
#include <QDateTime>
#include "db/DeviceInfo.h"
#include "db/BankProfileInfo.h"
namespace Ozon { namespace Ozon {
@ -11,6 +14,8 @@ public:
QString deviceId, QString deviceId,
QString appCode, QString appCode,
int maxCount = 0, int maxCount = 0,
double searchAmount = 0,
QDateTime searchTime = {},
QObject *parent = nullptr QObject *parent = nullptr
); );
@ -20,9 +25,20 @@ protected:
void doStart() override; void doStart() override;
private: 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 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; QString m_error;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -81,178 +81,143 @@ 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() { void DatabaseManager::initSchema() {
QSqlDatabase db = database(); QSqlDatabase db = database();
QSqlQuery q(db); QSqlQuery q(db);
// Читаем текущую версию схемы db.transaction();
q.exec("PRAGMA user_version");
const int version = q.next() ? q.value(0).toInt() : 0;
// ── Версия 1: полная схема ───────────────────────────────────────────────── q.exec(R"(CREATE TABLE IF NOT EXISTS devices (
if (version < 1) { id TEXT PRIMARY KEY,
db.transaction(); api_id TEXT,
adb_serial TEXT,
status TEXT,
name TEXT,
android TEXT,
screen_width INTEGER,
screen_height INTEGER,
battery INTEGER,
update_time DATETIME,
timestamp DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
image BLOB
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS devices ( q.exec(R"(CREATE TABLE IF NOT EXISTS bank_profiles (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
api_id TEXT, device_id TEXT,
status TEXT, pin_code TEXT,
name TEXT, pin_code_checked TEXT DEFAULT 'no',
android TEXT, name TEXT,
screen_width INTEGER, code TEXT,
screen_height INTEGER, package TEXT,
battery INTEGER, status TEXT DEFAULT 'off',
update_time DATETIME, install TEXT DEFAULT 'no',
timestamp DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), comment TEXT,
image BLOB 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)
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS applications ( q.exec(R"(CREATE TABLE IF NOT EXISTS materials (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT, device_id TEXT,
pin_code TEXT, app_code TEXT,
pin_code_checked TEXT DEFAULT 'no', last_numbers TEXT,
name TEXT, description TEXT,
code TEXT, amount REAL,
package TEXT, currency TEXT DEFAULT '',
status TEXT DEFAULT 'off', update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
install TEXT DEFAULT 'no', status TEXT DEFAULT 'active',
comment TEXT, card_number TEXT DEFAULT '',
phone TEXT DEFAULT '', material_id TEXT DEFAULT '',
email TEXT DEFAULT '', FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
full_name TEXT DEFAULT '', UNIQUE (device_id, app_code, last_numbers)
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)
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS accounts ( q.exec(R"(CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT, account_id INTEGER,
app_code TEXT, external_id INTEGER,
last_numbers TEXT, amount REAL,
description TEXT, fee REAL,
amount REAL, status TEXT,
currency TEXT DEFAULT '', type TEXT,
update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), phone TEXT,
status TEXT DEFAULT 'active', name TEXT,
card_number TEXT DEFAULT '', short_description TEXT,
material_id TEXT DEFAULT '', updated_short_description TEXT,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, description TEXT,
UNIQUE (device_id, app_code, last_numbers) updated_description TEXT,
))"); bank_name TEXT,
bank_time TEXT,
bank_tr_external_id TEXT,
update_time DATETIME,
complete_time DATETIME,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
receipt_image BLOB,
FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS transactions ( q.exec(R"(CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER, account_id INTEGER,
external_id INTEGER, external_id TEXT,
amount REAL, device_id TEXT,
fee REAL, amount REAL,
status TEXT, phone TEXT,
type TEXT, bank_name TEXT,
phone TEXT, comment TEXT,
name TEXT, status TEXT,
short_description TEXT, type TEXT,
updated_short_description TEXT, update_time DATETIME,
description TEXT, complete_time DATETIME,
updated_description TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
bank_name TEXT, json TEXT DEFAULT '',
bank_time TEXT, sent_to_server INTEGER DEFAULT 0,
bank_tr_external_id TEXT, screenshot BLOB,
update_time DATETIME, FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE,
complete_time DATETIME, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, ))");
receipt_image BLOB,
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS events ( q.exec(R"(CREATE TABLE IF NOT EXISTS banks (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER, alias TEXT UNIQUE,
external_id TEXT, name TEXT,
device_id TEXT, nspk_name TEXT
amount REAL, ))");
phone TEXT,
bank_name TEXT,
comment TEXT,
status TEXT,
type TEXT,
update_time DATETIME,
complete_time DATETIME,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
json TEXT DEFAULT '',
sent_to_server INTEGER DEFAULT 0,
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS banks ( q.exec(R"(CREATE TABLE IF NOT EXISTS app_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
alias TEXT UNIQUE, source TEXT NOT NULL DEFAULT '',
name TEXT, device_id TEXT NOT NULL DEFAULT '',
nspk_name TEXT message TEXT NOT NULL,
))"); screenshot BLOB,
timestamp TEXT NOT NULL
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS app_logs ( q.exec("CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC)");
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
))");
q.exec("CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC)"); q.exec(R"(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
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS settings ( q.exec("CREATE INDEX IF NOT EXISTS idx_general_logs_id ON general_logs (id DESC)");
key TEXT PRIMARY KEY,
value TEXT
))");
db.commit(); q.exec(R"(CREATE TABLE IF NOT EXISTS settings (
q.exec("PRAGMA user_version = 1"); key TEXT PRIMARY KEY,
qDebug() << "[DB] Migration 1 applied"; value TEXT
} ))");
// ── Версия 2: добавляем email в applications ───────────────────────────── db.commit();
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 '',
source TEXT NOT NULL DEFAULT '',
message TEXT NOT NULL,
timestamp TEXT NOT NULL
))");
q.exec("CREATE INDEX IF NOT EXISTS idx_general_logs_id ON general_logs (id DESC)");
db.commit();
q.exec("PRAGMA user_version = 3");
qDebug() << "[DB] Migration 3 applied";
}
} }
void DatabaseManager::closeConnection() { void DatabaseManager::closeConnection() {

View File

@ -68,3 +68,30 @@ QByteArray AppLogDAO::getScreenshot(int id) {
if (!q.exec() || !q.next()) return {}; if (!q.exec() || !q.next()) return {};
return q.value(0).toByteArray(); 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) // Загружает скриншот отдельно по id (по клику в UI)
[[nodiscard]] static QByteArray getScreenshot(int id); [[nodiscard]] static QByteArray getScreenshot(int id);
// Все записи со скриншотами (для экспорта)
[[nodiscard]] static QList<AppLogEntry> getLogsWithScreenshots(int limit = 100);
}; };

View File

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

View File

@ -1,10 +1,10 @@
#pragma once #pragma once
#include "db/ApplicationInfo.h" #include "db/BankProfileInfo.h"
class ApplicationDAO { class BankProfileDAO {
public: 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); [[nodiscard]] static bool updatePinCode(const QString &appId, const QString &pinCode);
@ -21,6 +21,8 @@ public:
const QString &comment const QString &comment
); );
[[nodiscard]] static bool updateComment(int appId, const QString &comment);
[[nodiscard]] static bool updateProfileInfo( [[nodiscard]] static bool updateProfileInfo(
const int &appId, const int &appId,
const QString &fullName, const QString &fullName,
@ -30,13 +32,15 @@ public:
[[nodiscard]] static bool updateBankProfileId(int appId, const QString &bankProfileId); [[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 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); [[nodiscard]] static bool checkInstalledApps(const QString &deviceId, const QSet<QString> &apps);

View File

@ -9,6 +9,7 @@ DeviceInfo parseDevice(const QSqlQuery &query) {
DeviceInfo device; DeviceInfo device;
device.id = query.value("id").toString(); device.id = query.value("id").toString();
device.apiId = query.value("api_id").toString(); device.apiId = query.value("api_id").toString();
device.adbSerial = query.value("adb_serial").toString();
device.status = query.value("status").toString(); device.status = query.value("status").toString();
device.name = query.value("name").toString(); device.name = query.value("name").toString();
device.android = query.value("android").toString(); device.android = query.value("android").toString();
@ -25,7 +26,7 @@ QList<DeviceInfo> DeviceDAO::getAllDevices(const bool online) {
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
QString sql = R"( 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 FROM devices
)"; )";
if (online) { if (online) {
@ -71,7 +72,7 @@ DeviceInfo DeviceDAO::getDeviceById(const QString &deviceId) {
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( 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 FROM devices
WHERE id = :deviceId WHERE id = :deviceId
)"); )");
@ -89,6 +90,69 @@ DeviceInfo DeviceDAO::getDeviceById(const QString &deviceId) {
return {}; 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) { bool DeviceDAO::upsertDevice(const DeviceInfo &device) {
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
@ -107,6 +171,8 @@ bool DeviceDAO::upsertDevice(const DeviceInfo &device) {
// Обновление // Обновление
query.prepare(R"( query.prepare(R"(
UPDATE devices SET 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, status = :status,
name = :name, name = :name,
android = :android, android = :android,
@ -120,14 +186,16 @@ bool DeviceDAO::upsertDevice(const DeviceInfo &device) {
// Вставка // Вставка
query.prepare(R"( query.prepare(R"(
INSERT INTO devices ( 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 ( ) 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(":id", device.id);
query.bindValue(":api_id", device.apiId);
query.bindValue(":adb_serial", device.adbSerial);
query.bindValue(":status", device.status); query.bindValue(":status", device.status);
query.bindValue(":name", device.name); query.bindValue(":name", device.name);
query.bindValue(":android", device.android); query.bindValue(":android", device.android);

View File

@ -6,6 +6,12 @@ class DeviceDAO {
public: public:
[[nodiscard]] static DeviceInfo getDeviceById(const QString &deviceId); [[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 upsertDevice(const DeviceInfo &device);
[[nodiscard]] static bool updateApiId(const QString &deviceId, const QString &apiId); [[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 status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
FROM events FROM events
WHERE status = :status AND device_id = :device_id 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 LIMIT 1
)"); )");
query.bindValue(":status", eventStatusToString(EventStatus::Wait)); query.bindValue(":status", eventStatusToString(EventStatus::Wait));
@ -258,3 +265,78 @@ bool EventDAO::existsActiveByExternalId(const QString &externalId) {
} }
return query.next(); 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 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 updateEventAmount(int id, double amount);
[[nodiscard]] static bool markSentToServer(int id); [[nodiscard]] static bool markSentToServer(int id);

View File

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

View File

@ -1,10 +1,10 @@
#pragma once #pragma once
#include <QSet> #include <QSet>
#include "db/AccountInfo.h" #include "db/MaterialInfo.h"
class AccountDAO { class MaterialDAO {
public: public:
[[nodiscard]] static bool upsertAccount(const AccountInfo &info); [[nodiscard]] static bool upsertAccount(const MaterialInfo &info);
[[nodiscard]] static bool updateAccountById( [[nodiscard]] static bool updateAccountById(
int id, int id,
@ -19,19 +19,19 @@ public:
[[nodiscard]] static bool updateMaterialId(int accountId, const QString &materialId); [[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 &deviceId,
const QString &appName, const QString &appName,
const QString &lastNumber const QString &lastNumber
); );
[[nodiscard]] static AccountInfo findByMaterialId(const QString &materialId); [[nodiscard]] static MaterialInfo findByMaterialId(const QString &materialId);
[[nodiscard]] static QStringList getActiveMaterialIds(); [[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 <QApplication>
#include <QThread> #include <QThread>
#include "dao/AccountDAO.h" #include "dao/MaterialDAO.h"
#include "views/MainWindow.h" #include "views/MainWindow.h"
#include "database/DatabaseManager.h" #include "database/DatabaseManager.h"
@ -27,6 +27,7 @@
#include "black/PayByCardScript.h" #include "black/PayByCardScript.h"
#include "ozon/PayByPhoneScript.h" #include "ozon/PayByPhoneScript.h"
#include "widget/loader/ErrorWindow.h" #include "widget/loader/ErrorWindow.h"
#include "widget/loader/LoaderDialog.h"
#include "widget/loader/LoaderWindow.h" #include "widget/loader/LoaderWindow.h"
#include "widget/loader/RegistrationWindow.h" #include "widget/loader/RegistrationWindow.h"
#include <QMessageBox> #include <QMessageBox>
@ -79,12 +80,12 @@ void startEventHandler(const QCoreApplication &app) {
void startPayByPhoneTest() { void startPayByPhoneTest() {
// Берём первый девайс с ozon аккаунтом // Берём первый девайс с ozon аккаунтом
QList<AccountInfo> accounts = AccountDAO::getAccountsByAppCode("ozon"); QList<MaterialInfo> accounts = MaterialDAO::getAccountsByAppCode("ozon");
if (accounts.isEmpty()) { if (accounts.isEmpty()) {
qWarning() << "[PayByPhoneTest] No ozon accounts found"; qWarning() << "[PayByPhoneTest] No ozon accounts found";
return; return;
} }
AccountInfo account = accounts.first(); MaterialInfo account = accounts.first();
qDebug() << "[PayByPhoneTest] Using account:" << account.id << account.deviceId << account.lastNumbers; qDebug() << "[PayByPhoneTest] Using account:" << account.id << account.deviceId << account.lastNumbers;
auto *thread = new QThread(nullptr); auto *thread = new QThread(nullptr);
@ -107,12 +108,12 @@ void startPayByPhoneTest() {
} }
void startBlackPayByCardTest() { void startBlackPayByCardTest() {
QList<AccountInfo> accounts = AccountDAO::getAccountsByAppCode("black"); QList<MaterialInfo> accounts = MaterialDAO::getAccountsByAppCode("black");
if (accounts.isEmpty()) { if (accounts.isEmpty()) {
qWarning() << "[BlackPayByCardTest] No black accounts found"; qWarning() << "[BlackPayByCardTest] No black accounts found";
return; return;
} }
AccountInfo account = accounts.first(); MaterialInfo account = accounts.first();
qDebug() << "[BlackPayByCardTest] Using account:" << account.id << account.deviceId << account.lastNumbers; qDebug() << "[BlackPayByCardTest] Using account:" << account.id << account.deviceId << account.lastNumbers;
auto *thread = new QThread(nullptr); auto *thread = new QThread(nullptr);
@ -143,7 +144,22 @@ void startNetworkService(const QCoreApplication &app) {
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater); QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater);
QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() { 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(); networkService->stop();
loop.exec();
loader->close();
delete loader;
}); });
// Сигнал эмитируется из фонового потока → Qt::QueuedConnection доставит его в главный поток // Сигнал эмитируется из фонового потока → Qt::QueuedConnection доставит его в главный поток

View File

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

View File

@ -3,8 +3,9 @@
#include <QDateTime> #include <QDateTime>
struct DeviceInfo { 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 apiId; // UUID from backend API (DeviceResponseModel.id)
QString adbSerial; // ADB serial (e.g. "emulator-5554", "7dfc444d43fbeab7")
QString status; QString status;
QString name; QString name;
QString android; 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 <db/DeviceInfo.h>
#include "adb/AdbUtils.h" #include "adb/AdbUtils.h"
#include "dao/ApplicationDAO.h" #include "dao/BankProfileDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/SettingsDAO.h" #include "dao/SettingsDAO.h"
DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) { DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) {
@ -181,12 +182,13 @@ QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) {
} }
QJsonObject body; QJsonObject body;
body["desktop_id"] = desktopId; body["desktop_id"] = desktopId;
body["status"] = "ONLINE"; body["bank_profile_id"] = QJsonValue::Null;
body["name"] = device.name; body["status"] = "ONLINE";
body["screen_width"] = device.screenWidth; body["name"] = device.name;
body["screen_height"] = device.screenHeight; body["screen_width"] = device.screenWidth;
body["battery"] = device.battery; body["screen_height"] = device.screenHeight;
body["battery"] = device.battery;
const QJsonValue result = apiPost("/api/v1/device/", body); const QJsonValue result = apiPost("/api/v1/device/", body);
if (!result.isNull() && !result.isUndefined()) { if (!result.isNull() && !result.isUndefined()) {
@ -239,25 +241,42 @@ void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray
} }
void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) { 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; QJsonObject body;
body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE"; body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE";
body["battery"] = device.battery; body["bank_profile_id"] = bankProfileId;
body["update_time"] = QDateTime::currentSecsSinceEpoch(); body["battery"] = device.battery;
body["update_time"] = QDateTime::currentSecsSinceEpoch();
apiPatch("/api/v1/device/" + device.apiId, body); apiPatch("/api/v1/device/" + device.apiId, body);
} }
void DeviceScreener::toggleBankProfilesOnApi(const QString &deviceId, bool enable) { void DeviceScreener::toggleBankProfilesOnApi(const QString &deviceId, bool enable) {
const QList<ApplicationInfo> apps = ApplicationDAO::getApplicationsByDeviceId(deviceId); if (enable) return; // не включаем автоматически при появлении устройства
for (const ApplicationInfo &app : apps) {
const QList<BankProfileInfo> apps = BankProfileDAO::getAllApplicationsByDeviceId(deviceId);
for (const BankProfileInfo &app : apps) {
if (app.bankProfileId.isEmpty()) continue; if (app.bankProfileId.isEmpty()) continue;
if (app.status != "active") continue;
// При включении — только если приложение активно в системе apiPatch("/api/v1/profile/bank_profile/" + app.bankProfileId + "/false", QJsonObject());
if (enable && app.status != "active") continue;
const QString enableStr = enable ? "true" : "false"; const QList<MaterialInfo> materials = MaterialDAO::getAccounts(deviceId, app.code);
const QString path = "/api/v1/profile/bank_profile/" + app.bankProfileId + "/" + enableStr; for (const MaterialInfo &mat : materials) {
apiPatch(path, QJsonObject()); if (mat.materialId.isEmpty()) continue;
apiPatch("/api/v1/profile/material/" + mat.materialId + "/false", QJsonObject());
}
} }
BankProfileDAO::deactivateAppsForDevice(deviceId);
} }
QJsonValue DeviceScreener::apiGet(const QString &path) { QJsonValue DeviceScreener::apiGet(const QString &path) {
@ -304,24 +323,39 @@ void DeviceScreener::start() {
QList<QPair<QString, QString> > devices = readAdbDevices(); QList<QPair<QString, QString> > devices = readAdbDevices();
QStringList onlineIds; QStringList onlineIds;
QList<DeviceInfo> deviceInfos; QList<DeviceInfo> deviceInfos;
for (const auto &[id, status]: devices) { for (const auto &[adbSerial, status]: devices) {
DeviceInfo device; DeviceInfo device;
device.id = id; bool isChecked = true;
if (status == "device") { 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; onlineIds << device.id;
const bool isChecked = prevOnline.contains(id); isChecked = prevOnline.contains(device.id);
device = readDeviceInfo(id, isChecked);
device.image = takeScreenshot(id); device.image = takeScreenshot(adbSerial);
if (!device.image.isEmpty()) { if (!device.image.isEmpty()) {
if (!DeviceDAO::updateImage(id, device.image)) { if (!DeviceDAO::updateImage(device.id, device.image)) {
qDebug() << "Cant update screenshot"; qDebug() << "Cant update screenshot";
} }
} }
if (!isChecked) { } else {
// Новое устройство — включаем active профили device.id = adbSerial;
toggleBankProfilesOnApi(id, true);
}
} }
device.status = status; device.status = status;
device.updateTime = QDateTime::currentDateTime(); device.updateTime = QDateTime::currentDateTime();
@ -330,6 +364,16 @@ void DeviceScreener::start() {
if (!DeviceDAO::upsertDevice(device)) { if (!DeviceDAO::upsertDevice(device)) {
qDebug() << "Cant update 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 // Синхронизация с API
@ -361,10 +405,17 @@ void DeviceScreener::start() {
qDebug() << "Cant mark devices offline"; qDebug() << "Cant mark devices offline";
} }
// Было онлайн в БД, сейчас офлайн — выключаем профили на сервере // Было онлайн в БД, сейчас офлайн — выключаем профили и обновляем статус на сервере
for (const QString &id : prevOnline) { for (const QString &id : prevOnline) {
if (!onlineIds.contains(id)) { if (!onlineIds.contains(id)) {
toggleBankProfilesOnApi(id, false); 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 screenRegex(R"(Physical size: (\d+)x(\d+))");
static const QRegularExpression batteryRegex(R"(level:\s*(\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; DeviceInfo info;
info.id = deviceId;
info.screenWidth = 0; info.screenWidth = 0;
info.screenHeight = 0; info.screenHeight = 0;
info.battery = 0; info.battery = 0;
@ -462,12 +512,17 @@ DeviceInfo DeviceScreener::readDeviceInfo(const QString &deviceId, const bool is
auto runAdbCommand = [&](const QStringList &args) -> QString { auto runAdbCommand = [&](const QStringList &args) -> QString {
QProcess process; QProcess process;
process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath()); process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath());
process.start(AdbUtils::adbPath(), QStringList{"-s", deviceId} + args); process.start(AdbUtils::adbPath(), QStringList{"-s", adbSerial} + args);
process.waitForFinished(); process.waitForFinished();
return QString::fromUtf8(process.readAllStandardOutput()).trimmed(); 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"}); 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(); 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(); info.updateTime = QDateTime::currentDateTime();
return info; return info;
} }

View File

@ -33,7 +33,7 @@ private:
static QByteArray takeScreenshot(const QString &deviceId); 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); void postDevicesData(const QList<DeviceInfo> &devices);

View File

@ -10,13 +10,13 @@
#include <QTimer> #include <QTimer>
#include <QTimeZone> #include <QTimeZone>
#include "dao/AccountDAO.h" #include "dao/MaterialDAO.h"
#include "dao/ApplicationDAO.h" #include "dao/BankProfileDAO.h"
#include "dao/BankDAO.h" #include "dao/BankDAO.h"
#include "dao/DeviceDAO.h" #include "dao/DeviceDAO.h"
#include "dao/EventDAO.h" #include "dao/EventDAO.h"
#include "dao/TransactionDAO.h" #include "dao/TransactionDAO.h"
#include "db/ApplicationInfo.h" #include "db/BankProfileInfo.h"
#include "adb/AdbUtils.h" #include "adb/AdbUtils.h"
#include "net/NetworkService.h" #include "net/NetworkService.h"
#include "black/GetLastTransactionsScript.h" #include "black/GetLastTransactionsScript.h"
@ -50,7 +50,7 @@ void EventHandler::start() {
// Запрашиваем новые задачи только для устройств без pending events // Запрашиваем новые задачи только для устройств без pending events
const QSet<QString> busyDevices = EventDAO::getDevicesWithPendingEvents(); const QSet<QString> busyDevices = EventDAO::getDevicesWithPendingEvents();
const QStringList materialIds = AccountDAO::getActiveMaterialIdsExcludingDevices(busyDevices); const QStringList materialIds = MaterialDAO::getActiveMaterialIdsExcludingDevices(busyDevices);
if (!materialIds.isEmpty()) { if (!materialIds.isEmpty()) {
m_network->getTasks(materialIds); m_network->getTasks(materialIds);
} }
@ -99,13 +99,18 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
} }
// Ищем аккаунт по material_id напрямую // Ищем аккаунт по material_id напрямую
const AccountInfo account = AccountDAO::findByMaterialId(materialId); const MaterialInfo account = MaterialDAO::findByMaterialId(materialId);
if (account.id == -1) { if (account.id == -1) {
qWarning() << "[EventHandler] Account not found for bank:" << bankName << "material_id:" << materialId; qWarning() << "[EventHandler] Account not found for bank:" << bankName << "material_id:" << materialId;
sendTaskError(typeStr, materialId, bankName, "Account not found for material_id: " + materialId); sendTaskError(typeStr, materialId, bankName, "Account not found for material_id: " + materialId);
continue; continue;
} }
// FETCH_PROFILE: отменяем старые ожидающие задачи такого же типа на этом устройстве
if (eventType == EventType::FetchProfile) {
EventDAO::cancelWaitingByType(account.deviceId, EventType::FetchProfile);
}
EventInfo event; EventInfo event;
event.status = EventStatus::Wait; event.status = EventStatus::Wait;
event.type = eventType; 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) { 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; const EventStatus newStatus = result.isEmpty() ? EventStatus::Complete : EventStatus::Error;
if (!EventDAO::updateEvent(event.id, newStatus, result)) { if (!EventDAO::updateEvent(event.id, newStatus, result)) {
qCritical() << "[EventHandler] Failed to update event:" << event.id; qCritical() << "[EventHandler] Failed to update event:" << event.id;
} else { } else {
if (newStatus == EventStatus::Error) { 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); sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result);
EventDAO::markSentToServer(event.id); EventDAO::markSentToServer(event.id);
} }
if (newStatus != EventStatus::Error) { if (newStatus != EventStatus::Error) {
const AccountInfo account = AccountDAO::getAccountById(event.accountId); const MaterialInfo account = MaterialDAO::getAccountById(event.accountId);
const QMap<QString, int> currencyCodes = { const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32} {"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); 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 auto thr = new QThread(this);
const QString key = account.deviceId; const QString key = account.deviceId;
@ -373,17 +404,27 @@ void EventHandler::startThreadForTask(const AccountInfo &account, const EventInf
}; };
const QString &appCode = account.appCode; 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 (event.type == EventType::FetchProfile) {
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(deviceId, appCode)); if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true));
else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(deviceId, appCode)); else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(adbSerial, appCode));
else qWarning() << "[EventHandler] FETCH_PROFILE: unknown appCode:" << appCode; else qWarning() << "[EventHandler] FETCH_PROFILE: unknown appCode:" << appCode;
} }
if (event.type == EventType::FetchTransactions) { if (event.type == EventType::FetchTransactions) {
if (appCode == "ozon") setup(new Ozon::GetLastTransactionsScript(deviceId, appCode)); // Парсим amount и created_at из body задачи
else if (appCode == "black") setup(new Black::GetLastTransactionsScript(deviceId, appCode)); 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; else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
} }
@ -448,8 +489,8 @@ void EventHandler::processNextTask(const QString &deviceId) {
continue; continue;
} }
const AccountInfo account = AccountDAO::getAccountById(event.accountId); const MaterialInfo account = MaterialDAO::getAccountById(event.accountId);
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, account.appCode); const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, account.appCode);
if (app.status != "active") { if (app.status != "active") {
qWarning() << "[EventHandler] Bank profile disabled for" << account.appCode << "device:" << deviceId; qWarning() << "[EventHandler] Bank profile disabled for" << account.appCode << "device:" << deviceId;
EventDAO::updateEvent(event.id, EventStatus::Error, "Bank profile is disabled"); EventDAO::updateEvent(event.id, EventStatus::Error, "Bank profile is disabled");

View File

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

View File

@ -11,8 +11,9 @@
#include <QTimer> #include <QTimer>
#include <QTimeZone> #include <QTimeZone>
#include "dao/AccountDAO.h" #include "dao/BankDAO.h"
#include "dao/ApplicationDAO.h" #include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h" #include "dao/DeviceDAO.h"
#include "dao/EventDAO.h" #include "dao/EventDAO.h"
#include "dao/SettingsDAO.h" #include "dao/SettingsDAO.h"
@ -181,7 +182,7 @@ void NetworkService::getPayments() {
QString deviceId = obj["deviceId"].toString(); QString deviceId = obj["deviceId"].toString();
QString appName = obj["appName"].toString(); QString appName = obj["appName"].toString();
QString lastNumbers = obj["lastNumbers"].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.accountId = account.id;
eventInfo.deviceId = account.deviceId; eventInfo.deviceId = account.deviceId;
@ -291,10 +292,37 @@ void NetworkService::loginAndSetup() {
const qint64 expiresIn = static_cast<qint64>(loginData["expires_in"].toDouble()); const qint64 expiresIn = static_cast<qint64>(loginData["expires_in"].toDouble());
SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn);
// Шаг 2: проверяем есть ли desktop_id // Шаг 2: получаем актуальный desktop с сервера
const QString desktopId = SettingsDAO::get("desktop_id"); 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()) { if (desktopId.isEmpty()) {
// Шаг 3: создаём desktop // Десктопов на сервере нет — создаём новый
QJsonObject desktopBody; QJsonObject desktopBody;
desktopBody["blue_token"] = QJsonValue::Null; desktopBody["blue_token"] = QJsonValue::Null;
desktopBody["name"] = QJsonValue::Null; desktopBody["name"] = QJsonValue::Null;
@ -310,13 +338,38 @@ void NetworkService::loginAndSetup() {
} }
const QJsonObject desktopData = desktopResult.toObject(); 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()); SettingsDAO::set("profile_id", desktopData["profile_id"].toString());
} }
// Шаг 4: синхронизация устройств и профилей // Шаг 3: синхронизация устройств с сервера
syncDevicesFromServer(desktopId);
// Шаг 4: синхронизация банковских профилей и материалов с сервера
syncBankProfilesFromServer();
// Шаг 5: загрузка справочника банков если пустой
syncBanksIfEmpty();
// Шаг 6: синхронизация статусов профилей/материалов с учётом онлайн-устройств
syncCurrentProfile(); 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 finishedWithResult(1);
emit finished(); emit finished();
} }
@ -412,6 +465,49 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod
return result; 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) { QJsonValue NetworkService::getJson(const QString &path, bool withAuth) {
const QUrl url(path); const QUrl url(path);
QNetworkRequest request(url); QNetworkRequest request(url);
@ -457,6 +553,255 @@ QJsonValue NetworkService::getJson(const QString &path, bool withAuth) {
return result; 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() { void NetworkService::syncCurrentProfile() {
// 1. Определяем онлайн-устройства по БД (DeviceScreener обновит позже) // 1. Определяем онлайн-устройства по БД (DeviceScreener обновит позже)
const QList<DeviceInfo> allDevices = DeviceDAO::getAllDevices(false); const QList<DeviceInfo> allDevices = DeviceDAO::getAllDevices(false);
@ -492,8 +837,8 @@ void NetworkService::syncCurrentProfile() {
const QString serverBpState = bp["state"].toString(); // "enabled" / "disabled" const QString serverBpState = bp["state"].toString(); // "enabled" / "disabled"
// Находим applications с этим bank_profile_id // Находим applications с этим bank_profile_id
const QList<ApplicationInfo> apps = ApplicationDAO::findAllByBankProfileId(bpId); const QList<BankProfileInfo> apps = BankProfileDAO::findAllByBankProfileId(bpId);
for (const ApplicationInfo &app : apps) { for (const BankProfileInfo &app : apps) {
const bool deviceOnline = onlineDeviceIds.contains(app.deviceId); const bool deviceOnline = onlineDeviceIds.contains(app.deviceId);
const QString localStatus = app.status; // "active" / "off" const QString localStatus = app.status; // "active" / "off"
const QString serverStatus = (serverBpState == "disabled") ? "off" : "active"; const QString serverStatus = (serverBpState == "disabled") ? "off" : "active";
@ -521,11 +866,11 @@ void NetworkService::syncCurrentProfile() {
const QString matId = mat["id"].toString(); const QString matId = mat["id"].toString();
const QString serverMatState = mat["state"].toString(); const QString serverMatState = mat["state"].toString();
const AccountInfo account = AccountDAO::findByMaterialId(matId); const MaterialInfo account = MaterialDAO::findByMaterialId(matId);
if (account.id < 0) continue; // не найден в локальной БД if (account.id < 0) continue; // не найден в локальной БД
const bool deviceOnline = onlineDeviceIds.contains(account.deviceId); 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"; const QString serverStatus = (serverMatState == "disabled") ? "off" : "active";
if (!deviceOnline) { if (!deviceOnline) {
@ -559,7 +904,7 @@ void NetworkService::postBankProfile() {
const QString bankProfileId = data["id"].toString(); const QString bankProfileId = data["id"].toString();
const int appId = m_extraData.value("app_id").toInt(); const int appId = m_extraData.value("app_id").toInt();
if (!bankProfileId.isEmpty() && appId > 0) { if (!bankProfileId.isEmpty() && appId > 0) {
ApplicationDAO::updateBankProfileId(appId, bankProfileId); BankProfileDAO::updateBankProfileId(appId, bankProfileId);
qDebug() << "[NetworkService] postBankProfile saved bank_profile_id:" << bankProfileId; qDebug() << "[NetworkService] postBankProfile saved bank_profile_id:" << bankProfileId;
} }
} }
@ -616,7 +961,7 @@ void NetworkService::postMaterial() {
const QString materialId = result.toObject()["id"].toString(); const QString materialId = result.toObject()["id"].toString();
const int accountId = m_extraData.value("account_id").toInt(); const int accountId = m_extraData.value("account_id").toInt();
if (!materialId.isEmpty() && accountId > 0) { 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; 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); QUrl url(m_apiBase + m_pathGetTasks);
QUrlQuery urlQuery; QUrlQuery urlQuery;
const QStringList ids = materialIds.isEmpty() ? AccountDAO::getActiveMaterialIds() : materialIds; const QStringList ids = materialIds.isEmpty() ? MaterialDAO::getActiveMaterialIds() : materialIds;
if (ids.isEmpty()) { if (ids.isEmpty()) {
m_fetchingTasks = false; m_fetchingTasks = false;
return; return;
@ -903,8 +1248,62 @@ void NetworkService::start() {
scheduleNextRefresh(); 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() { void NetworkService::stop() {
m_running = false; m_running = false;
// Эмитируем finished() в потоке NetworkService, чтобы корректно завершить QThread QMetaObject::invokeMethod(this, [this]() {
QMetaObject::invokeMethod(this, [this]() { emit finished(); }, Qt::QueuedConnection); disableAllProfilesOnServer();
emit finished();
}, Qt::QueuedConnection);
} }

View File

@ -60,6 +60,8 @@ public slots:
void start(); void start();
void stop(); void stop();
void disableAllProfilesOnServer();
void deleteDeviceFromServer();
signals: signals:
void finished(); void finished();
@ -103,8 +105,12 @@ private:
QJsonValue getJson(const QString &path, bool withAuth = false); QJsonValue getJson(const QString &path, bool withAuth = false);
QJsonValue postJson(const QString &path, const QJsonObject &body, 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 patchJson(const QString &path, const QJsonObject &body, bool withAuth = false);
QJsonValue deleteJson(const QString &path, bool withAuth = false);
bool refreshToken(); bool refreshToken();
bool ensureValidToken(); bool ensureValidToken();
void scheduleNextRefresh(); 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 *bankListAction = new QAction("Банки", this);
auto *tutorialPageAction = new QAction("Инструкция", this); auto *tutorialPageAction = new QAction("Инструкция", this);
auto *eventAction = new QAction("События", this); auto *eventAction = new QAction("События", this);
auto *errorLogAction = new QAction("Ошибки", this);
auto *fileLogAction = new QAction("Логи", this); auto *fileLogAction = new QAction("Логи", this);
menu->addAction(devicesPageAction); menu->addAction(devicesPageAction);
// menu->addAction(accountPageAction); // menu->addAction(accountPageAction);
menu->addAction(bankListAction); menu->addAction(bankListAction);
menu->addAction(eventAction); menu->addAction(eventAction);
menu->addAction(tutorialPageAction); menu->addAction(tutorialPageAction);
menu->addAction(errorLogAction);
menu->addAction(fileLogAction); menu->addAction(fileLogAction);
connect(devicesPageAction, &QAction::triggered, this, [=]() { connect(devicesPageAction, &QAction::triggered, this, [=]() {
@ -98,15 +96,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
m_eventWindow->activateWindow(); 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, [=]() { connect(fileLogAction, &QAction::triggered, this, [=]() {
if (!m_fileLogWindow) { if (!m_fileLogWindow) {
m_fileLogWindow = new FileLogWindow(this); m_fileLogWindow = new FileLogWindow(this);

View File

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

View File

@ -19,15 +19,15 @@
#include "black/LoginAndCheckAccountsScript.h" #include "black/LoginAndCheckAccountsScript.h"
#include "black/GetProfileInfoScript.h" #include "black/GetProfileInfoScript.h"
#include "black/GetCardInfoScript.h" #include "black/GetCardInfoScript.h"
#include "dao/AccountDAO.h" #include "dao/MaterialDAO.h"
#include "dao/ApplicationDAO.h" #include "dao/BankProfileDAO.h"
#include "bank/TransactionWindow.h" #include "bank/TransactionWindow.h"
#include "device/EditBankAccountDialog.h" #include "device/EditBankAccountDialog.h"
#include "device/EditBankDialog.h" #include "device/EditBankDialog.h"
#include "net/NetworkService.h" #include "net/NetworkService.h"
AccountSettingsWindow::AccountSettingsWindow( AccountSettingsWindow::AccountSettingsWindow(
QWidget *parent, QString deviceId, QString deviceName, const ApplicationInfo &app QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app
) : QWidget(parent, ) : QWidget(parent,
Qt::Window Qt::Window
| Qt::WindowTitleHint | Qt::WindowTitleHint
@ -90,7 +90,7 @@ AccountSettingsWindow::AccountSettingsWindow(
const QString comment = dlg.secondValue(); const QString comment = dlg.secondValue();
bool on = dlg.toggled(); 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"; qDebug() << "Cant update bank data";
} else { } else {
m_applicationInfo.pinCode = pinCode; m_applicationInfo.pinCode = pinCode;
@ -132,6 +132,17 @@ AccountSettingsWindow::AccountSettingsWindow(
m_profileLabel->setTextFormat(Qt::RichText); m_profileLabel->setTextFormat(Qt::RichText);
m_profileLabel->setStyleSheet("margin-bottom: 8px;"); m_profileLabel->setStyleSheet("margin-bottom: 8px;");
profileRow->addWidget(m_profileLabel); 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); layout->addLayout(profileRow);
QPushButton *soloButton = new QPushButton("Запустить проверку пин-кода"); QPushButton *soloButton = new QPushButton("Запустить проверку пин-кода");
@ -320,35 +331,6 @@ AccountSettingsWindow::AccountSettingsWindow(
layout->addWidget(tableWidget); 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(); tableWidget->resizeRowsToContents();
@ -365,11 +347,22 @@ AccountSettingsWindow::AccountSettingsWindow(
void AccountSettingsWindow::startCheckPinCode(const QString &appCode) { void AccountSettingsWindow::startCheckPinCode(const QString &appCode) {
QThread *thread = new QThread(nullptr); 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") { if (appCode == "ozon") {
auto *worker = new Ozon::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); auto *worker = new Ozon::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread); worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); 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, thread, &QThread::quit);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &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); auto *worker = new Black::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread); worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start); 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, thread, &QThread::quit);
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &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) { void AccountSettingsWindow::startGetLastTransactions(const QString &appCode, int maxCount) {
if (appCode == "ozon") { if (appCode == "ozon") {
QThread *thread = new QThread(nullptr); 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); worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Ozon::GetLastTransactionsScript::start); connect(thread, &QThread::started, worker, &Ozon::GetLastTransactionsScript::start);
connect(worker, &Ozon::GetLastTransactionsScript::finished, thread, &QThread::quit); 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) { 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()) { if (app.bankProfileId.isEmpty()) {
qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << code; qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << code;
return; return;
@ -477,7 +471,7 @@ void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString
thread->start(); thread->start();
} }
void AccountSettingsWindow::updateAccountData(const AccountInfo &account) { void AccountSettingsWindow::updateAccountData(const MaterialInfo &account) {
QJsonArray list; QJsonArray list;
QJsonObject obj; QJsonObject obj;
obj["appName"] = account.appCode; obj["appName"] = account.appCode;
@ -485,7 +479,7 @@ void AccountSettingsWindow::updateAccountData(const AccountInfo &account) {
obj["amount"] = account.amount; obj["amount"] = account.amount;
obj["description"] = account.description; obj["description"] = account.description;
obj["currency"] = account.currency; obj["currency"] = account.currency;
obj["status"] = accountStatusToString(account.status); obj["status"] = materialStatusToString(account.status);
list.append(obj); list.append(obj);
@ -502,11 +496,11 @@ void AccountSettingsWindow::updateAccountData(const AccountInfo &account) {
} }
void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) { 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->clearContents();
tableWidget->setRowCount(accounts.length()); tableWidget->setRowCount(accounts.length());
tableWidget->setColumnCount(8); tableWidget->setColumnCount(9);
tableWidget->setHorizontalHeaderLabels({ 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) { 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")); QTableWidgetItem *time = new QTableWidgetItem(account.updateTime.toString("dd.MM.yyyy HH:mm:ss"));
time->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); time->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
@ -554,18 +549,24 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
currency->setFlags(currency->flags() & ~Qt::ItemIsEditable); currency->setFlags(currency->flags() & ~Qt::ItemIsEditable);
tableWidget->setItem(k, 4, currency); 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->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
status->setFlags(status->flags() & ~Qt::ItemIsEditable); status->setFlags(status->flags() & ~Qt::ItemIsEditable);
if (account.status != AccountStatus::Active) { if (account.status != MaterialStatus::Active) {
status->setForeground(QBrush(Qt::red)); status->setForeground(QBrush(Qt::red));
} }
tableWidget->setItem(k, 5, status); 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); auto *deleteBankAccountBtn = new QPushButton("Удалить", tableWidget);
deleteBankAccountBtn->setStyleSheet("background-color: red; color: white;"); 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]() { connect(deleteBankAccountBtn, &QPushButton::clicked, this, [this, tableWidget, account]() {
QMessageBox msgBox(this); QMessageBox msgBox(this);
@ -576,7 +577,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
msgBox.setDefaultButton(cancelBtn); msgBox.setDefaultButton(cancelBtn);
msgBox.exec(); msgBox.exec();
if (msgBox.clickedButton() == deleteBtn) { if (msgBox.clickedButton() == deleteBtn) {
if (AccountDAO::deleteAccount(account.id)) { if (MaterialDAO::deleteAccount(account.id)) {
reloadContent(tableWidget); reloadContent(tableWidget);
tableWidget->resizeColumnsToContents(); tableWidget->resizeColumnsToContents();
} else { } else {
@ -587,7 +588,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
auto *transactionsBtn = new QPushButton("Транзакции", tableWidget); auto *transactionsBtn = new QPushButton("Транзакции", tableWidget);
transactionsBtn->setStyleSheet("background-color: #1565C0; color: white;"); transactionsBtn->setStyleSheet("background-color: #1565C0; color: white;");
tableWidget->setCellWidget(k, 7, transactionsBtn); tableWidget->setCellWidget(k, 8, transactionsBtn);
connect(transactionsBtn, &QPushButton::clicked, this, [this, account]() { connect(transactionsBtn, &QPushButton::clicked, this, [this, account]() {
auto *txWindow = new TransactionWindow(account.id, this); auto *txWindow = new TransactionWindow(account.id, this);

View File

@ -3,19 +3,19 @@
#include <QTableWidget> #include <QTableWidget>
#include <QWidget> #include <QWidget>
#include "db/AccountInfo.h" #include "db/MaterialInfo.h"
#include "db/ApplicationInfo.h" #include "db/BankProfileInfo.h"
class AccountSettingsWindow final : public QWidget { class AccountSettingsWindow final : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
AccountSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const ApplicationInfo &app); AccountSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app);
private: private:
QString m_deviceId; QString m_deviceId;
QString m_deviceName; QString m_deviceName;
ApplicationInfo m_applicationInfo; BankProfileInfo m_applicationInfo;
QLabel *m_statusLabel; QLabel *m_statusLabel;
QLabel *m_commentLabel; QLabel *m_commentLabel;
@ -33,5 +33,5 @@ private:
void sendAppStatus(const QString &deviceId, const QString &code, const QString &status); 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 <QScrollArea>
#include "TransactionWindow.h" #include "TransactionWindow.h"
#include "dao/AccountDAO.h" #include "dao/MaterialDAO.h"
#include "dao/DeviceDAO.h" #include "dao/DeviceDAO.h"
AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) { AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) {
@ -30,7 +30,7 @@ AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) {
QList<DeviceInfo> devices = DeviceDAO::getAllDevices(false); QList<DeviceInfo> devices = DeviceDAO::getAllDevices(false);
for (int i = 0; i < devices.size(); ++i) { for (int i = 0; i < devices.size(); ++i) {
DeviceInfo device = devices[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); QString title = QString("%1 [%2] | %3").arg(device.name, device.id, device.status);
QLabel *label = new QLabel(title, this); QLabel *label = new QLabel(title, this);
@ -54,14 +54,14 @@ AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) {
for (int k = 0; k < accounts.size(); ++k) { for (int k = 0; k < accounts.size(); ++k) {
AccountInfo account = accounts[k]; MaterialInfo account = accounts[k];
QTableWidgetItem *item = new QTableWidgetItem("*" + account.lastNumbers); QTableWidgetItem *item = new QTableWidgetItem("*" + account.lastNumbers);
item->setData(Qt::UserRole, account.id); item->setData(Qt::UserRole, account.id);
tableWidget->setItem(k, 0, item); tableWidget->setItem(k, 0, item);
tableWidget->setItem(k, 1, new QTableWidgetItem(QString::number(account.amount))); 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, 2, new QTableWidgetItem(account.updateTime.toString("dd.MM.yyyy HH:mm:ss")));
tableWidget->setItem(k, 3, new QTableWidgetItem(account.description)); 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); totalHeight += tableWidget->rowHeight(k);
} }
tableWidget->setFixedHeight(totalHeight + 2); tableWidget->setFixedHeight(totalHeight + 2);

View File

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

View File

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

View File

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

View File

@ -7,11 +7,12 @@
#include <QPushButton> #include <QPushButton>
#include <QLabel> #include <QLabel>
#include <QSettings>
#include "bank/AccountSettingsWindow.h" #include "bank/AccountSettingsWindow.h"
#include "dao/ApplicationDAO.h" #include "dao/BankProfileDAO.h"
#include "db/ApplicationInfo.h" #include "db/BankProfileInfo.h"
static QString formatStatus(const ApplicationInfo &app) { static QString formatStatus(const BankProfileInfo &app) {
if (!app.install) return "не установлен"; if (!app.install) return "не установлен";
if (app.status == "active") { if (app.status == "active") {
if (!app.pinCodeChecked) if (!app.pinCodeChecked)
@ -63,7 +64,29 @@ DeviceSettingsWindow::DeviceSettingsWindow(QWidget *parent, const QString &devic
} }
void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) { 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->clearContents();
tableWidget->setRowCount(apps.size()); tableWidget->setRowCount(apps.size());
@ -76,7 +99,7 @@ void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) {
tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch); tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch);
for (int k = 0; k < apps.size(); ++k) { for (int k = 0; k < apps.size(); ++k) {
const ApplicationInfo &app = apps[k]; const BankProfileInfo &app = apps[k];
// Иконка // Иконка
auto *icon = new QLabel(tableWidget); auto *icon = new QLabel(tableWidget);

View File

@ -5,6 +5,8 @@
#include <QImage> #include <QImage>
#include <QPushButton> #include <QPushButton>
#include <QCoreApplication> #include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPointer> #include <QPointer>
#include <QPropertyAnimation> #include <QPropertyAnimation>
#include <QGraphicsScene> #include <QGraphicsScene>
@ -13,14 +15,15 @@
#include "bank/AccountSettingsWindow.h" #include "bank/AccountSettingsWindow.h"
#include "bank/BankSettingsWindow.h" #include "bank/BankSettingsWindow.h"
#include "DeviceSettingsWindow.h" #include "DeviceSettingsWindow.h"
#include "dao/ApplicationDAO.h" #include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h" #include "dao/DeviceDAO.h"
#include "db/ApplicationInfo.h" #include "dao/EventDAO.h"
#include "db/BankProfileInfo.h"
#include "net/NetworkService.h" #include "net/NetworkService.h"
#include <QThread> #include <QThread>
QString getColorByStatus(const ApplicationInfo &app) { QString getColorByStatus(const BankProfileInfo &app) {
if (!app.install) { if (!app.install) {
return "black"; 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) { 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()) { if (app.bankProfileId.isEmpty()) {
qWarning() << "[DeviceWidget] sendBankStatus: no bankProfileId for" << code; qWarning() << "[DeviceWidget] sendBankStatus: no bankProfileId for" << code;
return; 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"); const bool enable = (status == "active");
auto *thread = new QThread; auto *thread = new QThread;
auto *net = new NetworkService; auto *net = new NetworkService;
@ -56,7 +90,7 @@ void sendBankStatus(const QString &deviceId, const QString &code, const QString
thread->start(); 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->setContentsMargins(0, 0, 0, 0);
rowWidget->setStyleSheet( rowWidget->setStyleSheet(
"background-color: rgba(0, 0, 0, 0.6);" // полупрозрачный фон "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( label->setStyleSheet(
"background-color: transparent;" "background-color: transparent;"
"margin: 0px;" "margin: 0px;"
@ -129,7 +172,7 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget,
msgBox.exec(); msgBox.exec();
if (msgBox.clickedButton() == confirmBtn) { 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"; qDebug() << "Cant update bank data";
} else { } else {
sendBankStatus(app.deviceId, app.code, newStatus); sendBankStatus(app.deviceId, app.code, newStatus);
@ -175,7 +218,7 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget,
// hbox->addStretch(); // hbox->addStretch();
} }
void DeviceWidget::setOpenBanksSettings(QPushButton *button, const ApplicationInfo &app) { void DeviceWidget::setOpenBanksSettings(QPushButton *button, const BankProfileInfo &app) {
QString deviceId = m_device.id; QString deviceId = m_device.id;
QString deviceName = m_device.name; QString deviceName = m_device.name;
connect(button, &QPushButton::clicked, this, [deviceId, deviceName, app]() { connect(button, &QPushButton::clicked, this, [deviceId, deviceName, app]() {
@ -219,6 +262,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
statusLabel->setStyleSheet( statusLabel->setStyleSheet(
"background-color: rgba(0, 0, 0, 0.5);" "background-color: rgba(0, 0, 0, 0.5);"
"color: white;" "color: white;"
"font-size: 10px;"
); );
layout->addWidget(statusLabel, 0, 0, Qt::AlignTop | Qt::AlignLeft); layout->addWidget(statusLabel, 0, 0, Qt::AlignTop | Qt::AlignLeft);
@ -274,7 +318,15 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
msgBox.exec(); msgBox.exec();
if (msgBox.clickedButton() == confirmBtn) { 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"; const bool isOffline = device.status != "device";
QList<ApplicationInfo> apps = ApplicationDAO::getApplicationsByDeviceId(device.id); QList<BankProfileInfo> apps = BankProfileDAO::getApplicationsByDeviceId(device.id);
for (ApplicationInfo &app: apps) { for (BankProfileInfo &app: apps) {
if (app.install) { if (app.install) {
auto *rowWidget = new QWidget(appsView); auto *rowWidget = new QWidget(appsView);
buildBankRow(app, rowWidget, appsView, isOffline); buildBankRow(app, rowWidget, appsView, isOffline);

View File

@ -3,7 +3,7 @@
#include <qtablewidget.h> #include <qtablewidget.h>
#include <QWidget> #include <QWidget>
#include "db/ApplicationInfo.h" #include "db/BankProfileInfo.h"
#include "db/DeviceInfo.h" #include "db/DeviceInfo.h"
class DeviceWidget final : public QWidget { class DeviceWidget final : public QWidget {
@ -15,9 +15,9 @@ public:
private: private:
const DeviceInfo m_device; 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: signals:
void deleteRequested(const QString &deviceId); void deleteRequested(const QString &deviceId);

View File

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

View File

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

View File

@ -67,7 +67,7 @@ static QString typeDisplayName(EventType t) {
EventWindow::EventWindow(QWidget *parent) : QDialog(parent) { EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
setWindowTitle("События"); setWindowTitle("События");
resize(1400, 600); resize(1400, 700);
// ── Таблица ─────────────────────────────────────────────────────────────── // ── Таблица ───────────────────────────────────────────────────────────────
m_table = new QTableWidget(this); 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_SENT, 45);
m_table->setColumnWidth(COL_ACTION, 80); 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_prevBtn = new QPushButton("", this);
m_nextBtn = new QPushButton("", this); m_nextBtn = new QPushButton("", this);
@ -108,7 +121,7 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
paginationLayout->addWidget(refreshBtn); paginationLayout->addWidget(refreshBtn);
auto *mainLayout = new QVBoxLayout(this); auto *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(m_table, 1); mainLayout->addWidget(splitter, 1);
mainLayout->addLayout(paginationLayout); mainLayout->addLayout(paginationLayout);
// ── Сигналы ─────────────────────────────────────────────────────────────── // ── Сигналы ───────────────────────────────────────────────────────────────
@ -122,6 +135,11 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
m_page = 0; m_page = 0;
loadPage(); 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) { connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int col) {
if (col != COL_JSON) return; if (col != COL_JSON) return;
auto *item = m_table->item(row, col); auto *item = m_table->item(row, col);
@ -150,9 +168,11 @@ void EventWindow::loadPage() {
m_page = qBound(0, m_page, m_totalPages - 1); m_page = qBound(0, m_page, m_totalPages - 1);
const QList<EventInfo> events = EventDAO::getEvents(m_page, m_pageSize); const QList<EventInfo> events = EventDAO::getEvents(m_page, m_pageSize);
m_eventIds.clear();
m_table->setRowCount(static_cast<int>(events.size())); m_table->setRowCount(static_cast<int>(events.size()));
for (int i = 0; i < events.size(); ++i) { for (int i = 0; i < events.size(); ++i) {
m_eventIds.append(events[i].id);
const auto &e = events[i]; const auto &e = events[i];
m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id))); m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id)));
m_table->setItem(i, COL_TIME, new QTableWidgetItem( m_table->setItem(i, COL_TIME, new QTableWidgetItem(
@ -204,6 +224,24 @@ void EventWindow::loadPage() {
updatePagination(); 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() { void EventWindow::updatePagination() {
m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages)); m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages));
m_prevBtn->setEnabled(m_page > 0); m_prevBtn->setEnabled(m_page > 0);

View File

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

View File

@ -1,10 +1,21 @@
#include "FileLogWindow.h" #include "FileLogWindow.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QHeaderView> #include <QHeaderView>
#include <QHttpMultiPart>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QProcess>
#include <QTemporaryFile>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "dao/EventDAO.h"
#include "dao/GeneralLogDAO.h" #include "dao/GeneralLogDAO.h"
#include "dao/SettingsDAO.h"
static constexpr int COL_ID = 0; static constexpr int COL_ID = 0;
static constexpr int COL_TIME = 1; static constexpr int COL_TIME = 1;
@ -38,6 +49,8 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
m_pageLabel->setMinimumWidth(140); m_pageLabel->setMinimumWidth(140);
auto *refreshBtn = new QPushButton("Обновить", this); 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; auto *paginationLayout = new QHBoxLayout;
paginationLayout->addWidget(m_prevBtn); paginationLayout->addWidget(m_prevBtn);
@ -46,6 +59,7 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
paginationLayout->addStretch(); paginationLayout->addStretch();
paginationLayout->addWidget(m_showAllCheck); paginationLayout->addWidget(m_showAllCheck);
paginationLayout->addWidget(refreshBtn); paginationLayout->addWidget(refreshBtn);
paginationLayout->addWidget(m_sendBtn);
auto *mainLayout = new QVBoxLayout(this); auto *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(m_table, 1); mainLayout->addWidget(m_table, 1);
@ -65,6 +79,7 @@ FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
m_page = 0; m_page = 0;
loadPage(); loadPage();
}); });
connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogToTelegram);
loadPage(); loadPage();
} }
@ -102,3 +117,141 @@ void FileLogWindow::updatePagination() {
m_prevBtn->setEnabled(m_page > 0); m_prevBtn->setEnabled(m_page > 0);
m_nextBtn->setEnabled(m_page < m_totalPages - 1); 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; QLabel *m_pageLabel;
QPushButton *m_prevBtn; QPushButton *m_prevBtn;
QPushButton *m_nextBtn; QPushButton *m_nextBtn;
QPushButton *m_sendBtn;
QCheckBox *m_showAllCheck; QCheckBox *m_showAllCheck;
int m_page = 0; int m_page = 0;
@ -25,4 +26,5 @@ private:
QStringList currentLevelFilter() const; QStringList currentLevelFilter() const;
void loadPage(); void loadPage();
void updatePagination(); void updatePagination();
void sendLogToTelegram();
}; };