Add PayByCardScript and PayByPhoneScript for Dushanbe automation, including transaction flow, OCR-based operation ID extraction, and enhanced logging.
@ -132,6 +132,7 @@ void GetCardInfoScript::doStart() {
|
|||||||
materialBody["bank_profile_id"] = updatedApp.bankProfileId;
|
materialBody["bank_profile_id"] = updatedApp.bankProfileId;
|
||||||
materialBody["card_number"] = savedAccount.cardNumber;
|
materialBody["card_number"] = savedAccount.cardNumber;
|
||||||
materialBody["name"] = updatedApp.fullName;
|
materialBody["name"] = updatedApp.fullName;
|
||||||
|
materialBody["state"] = materialStatusToString(savedAccount.status) == "active" ? "enabled" : "disabled";
|
||||||
ns.setPayload(QJsonDocument(materialBody).toJson());
|
ns.setPayload(QJsonDocument(materialBody).toJson());
|
||||||
ns.addExtra("account_id", savedAccount.id);
|
ns.addExtra("account_id", savedAccount.id);
|
||||||
ns.postMaterial();
|
ns.postMaterial();
|
||||||
|
|||||||
@ -174,8 +174,9 @@ void GetCardInfoScript::doStart() {
|
|||||||
ns.setDeviceId(m_deviceId);
|
ns.setDeviceId(m_deviceId);
|
||||||
QJsonObject materialBody;
|
QJsonObject materialBody;
|
||||||
materialBody["bank_profile_id"] = updatedApp.bankProfileId;
|
materialBody["bank_profile_id"] = updatedApp.bankProfileId;
|
||||||
materialBody["card_number"] = fullNumber;
|
materialBody["card_number"] = QString(fullNumber).remove(' ');
|
||||||
materialBody["name"] = updatedApp.fullName;
|
materialBody["name"] = updatedApp.fullName;
|
||||||
|
materialBody["state"] = materialStatusToString(savedAccount.status) == "active" ? "enabled" : "disabled";
|
||||||
ns.setPayload(QJsonDocument(materialBody).toJson());
|
ns.setPayload(QJsonDocument(materialBody).toJson());
|
||||||
ns.addExtra("account_id", savedAccount.id);
|
ns.addExtra("account_id", savedAccount.id);
|
||||||
ns.postMaterial();
|
ns.postMaterial();
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
#include "GetLastTransactionsScript.h"
|
#include "GetLastTransactionsScript.h"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QTimeZone>
|
#include <QTimeZone>
|
||||||
|
|
||||||
|
#include <tesseract/baseapi.h>
|
||||||
|
#include <leptonica/allheaders.h>
|
||||||
|
|
||||||
#include "adb/AdbUtils.h"
|
#include "adb/AdbUtils.h"
|
||||||
#include "dao/MaterialDAO.h"
|
#include "dao/MaterialDAO.h"
|
||||||
#include "dao/BankProfileDAO.h"
|
#include "dao/BankProfileDAO.h"
|
||||||
@ -10,6 +14,215 @@
|
|||||||
#include "dao/TransactionDAO.h"
|
#include "dao/TransactionDAO.h"
|
||||||
#include "AppLogger.h"
|
#include "AppLogger.h"
|
||||||
|
|
||||||
|
static QString recognizeTextFromImage(const QByteArray &pngData) {
|
||||||
|
auto *api = new tesseract::TessBaseAPI();
|
||||||
|
if (api->Init(nullptr, "rus+eng")) {
|
||||||
|
qWarning() << "[Dushanbe::OCR] Failed to init Tesseract";
|
||||||
|
delete api;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
|
||||||
|
static_cast<size_t>(pngData.size()));
|
||||||
|
if (!pix) {
|
||||||
|
api->End(); delete api;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
api->SetImage(pix);
|
||||||
|
char *text = api->GetUTF8Text();
|
||||||
|
QString result = QString::fromUtf8(text).trimmed();
|
||||||
|
delete[] text;
|
||||||
|
pixDestroy(&pix);
|
||||||
|
api->End();
|
||||||
|
delete api;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Точечный OCR: находит "Номер операции" через общий OCR, затем кропает строку правее
|
||||||
|
// и распознаёт только цифры. Работает с любым разрешением экрана.
|
||||||
|
static QString recognizeOperationId(const QByteArray &pngData) {
|
||||||
|
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
|
||||||
|
static_cast<size_t>(pngData.size()));
|
||||||
|
if (!pix) return {};
|
||||||
|
|
||||||
|
const int w = pixGetWidth(pix);
|
||||||
|
const int h = pixGetHeight(pix);
|
||||||
|
|
||||||
|
// 1. Полный OCR с координатами слов — ищем "операции"
|
||||||
|
auto *api = new tesseract::TessBaseAPI();
|
||||||
|
if (api->Init(nullptr, "rus+eng")) {
|
||||||
|
pixDestroy(&pix); delete api;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
api->SetImage(pix);
|
||||||
|
api->Recognize(nullptr);
|
||||||
|
|
||||||
|
int opLineY = -1;
|
||||||
|
int opLineH = 0;
|
||||||
|
bool prevWasNomer = false;
|
||||||
|
tesseract::ResultIterator *ri = api->GetIterator();
|
||||||
|
if (ri) {
|
||||||
|
do {
|
||||||
|
const char *word = ri->GetUTF8Text(tesseract::RIL_WORD);
|
||||||
|
if (word) {
|
||||||
|
const QString w_str = QString::fromUtf8(word);
|
||||||
|
delete[] word;
|
||||||
|
if (w_str.contains(QString::fromUtf8("омер")) ||
|
||||||
|
w_str.contains(QString::fromUtf8("omep"))) {
|
||||||
|
prevWasNomer = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (prevWasNomer &&
|
||||||
|
(w_str.contains(QString::fromUtf8("операции")) ||
|
||||||
|
w_str.contains(QString::fromUtf8("onepaции")))) {
|
||||||
|
int x1, y1, x2, y2;
|
||||||
|
ri->BoundingBox(tesseract::RIL_WORD, &x1, &y1, &x2, &y2);
|
||||||
|
opLineY = y1;
|
||||||
|
opLineH = y2 - y1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prevWasNomer = false;
|
||||||
|
}
|
||||||
|
} while (ri->Next(tesseract::RIL_WORD));
|
||||||
|
}
|
||||||
|
api->End();
|
||||||
|
delete api;
|
||||||
|
|
||||||
|
if (opLineY < 0) {
|
||||||
|
pixDestroy(&pix);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Кропаем правую половину этой строки и OCR только цифры
|
||||||
|
const int cropY = qMax(0, opLineY - opLineH / 2);
|
||||||
|
const int cropH = opLineH * 2;
|
||||||
|
BOX *box = boxCreate(w / 2, cropY, w / 2, qMin(cropH, h - cropY));
|
||||||
|
Pix *cropped = pixClipRectangle(pix, box, nullptr);
|
||||||
|
boxDestroy(&box);
|
||||||
|
pixDestroy(&pix);
|
||||||
|
|
||||||
|
if (!cropped) return {};
|
||||||
|
|
||||||
|
auto *api2 = new tesseract::TessBaseAPI();
|
||||||
|
if (api2->Init(nullptr, "eng")) {
|
||||||
|
pixDestroy(&cropped); delete api2;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
api2->SetVariable("tessedit_char_whitelist", "0123456789");
|
||||||
|
api2->SetPageSegMode(tesseract::PSM_SINGLE_LINE);
|
||||||
|
api2->SetImage(cropped);
|
||||||
|
char *text = api2->GetUTF8Text();
|
||||||
|
QString result = QString::fromUtf8(text).trimmed();
|
||||||
|
delete[] text;
|
||||||
|
pixDestroy(&cropped);
|
||||||
|
api2->End();
|
||||||
|
delete api2;
|
||||||
|
|
||||||
|
result.remove(QRegularExpression("[^0-9]"));
|
||||||
|
if (result.length() >= 8) {
|
||||||
|
qDebug() << "[Dushanbe::OCR] Precise operation ID:" << result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OcrTransactionDetail {
|
||||||
|
QString date;
|
||||||
|
QString time;
|
||||||
|
QString operationId;
|
||||||
|
QString provider;
|
||||||
|
QString senderAccount;
|
||||||
|
QString receiverAccount;
|
||||||
|
double amount = 0.0;
|
||||||
|
double fee = 0.0;
|
||||||
|
QString status;
|
||||||
|
};
|
||||||
|
|
||||||
|
static OcrTransactionDetail parseDetailFromOcr(const QString &ocrText) {
|
||||||
|
OcrTransactionDetail d;
|
||||||
|
const QStringList lines = ocrText.split('\n');
|
||||||
|
|
||||||
|
for (const QString &rawLine : lines) {
|
||||||
|
const QString line = rawLine.trimmed();
|
||||||
|
|
||||||
|
// "Дата операции: 05.04.2026" или "Дата операции:" на отдельной строке
|
||||||
|
if (line.contains(QString::fromUtf8("Дата операции"))) {
|
||||||
|
QRegularExpression re(R"((\d{2}\.\d{2}\.\d{4}))");
|
||||||
|
if (auto m = re.match(line); m.hasMatch()) d.date = m.captured(1);
|
||||||
|
}
|
||||||
|
// "Время операции: 18:19:54"
|
||||||
|
else if (line.contains(QString::fromUtf8("Время операции"))) {
|
||||||
|
QRegularExpression re(R"((\d{2}:\d{2}:\d{2}))");
|
||||||
|
if (auto m = re.match(line); m.hasMatch()) d.time = m.captured(1);
|
||||||
|
}
|
||||||
|
// "Номер операции: 1598279763"
|
||||||
|
else if (line.contains(QString::fromUtf8("омер операции"))) {
|
||||||
|
QRegularExpression re(R"((\d{5,}))");
|
||||||
|
if (auto m = re.match(line); m.hasMatch()) d.operationId = m.captured(1);
|
||||||
|
}
|
||||||
|
// "Поставщик: DC (по номеру карты)"
|
||||||
|
else if (line.contains(QString::fromUtf8("Поставщик"))) {
|
||||||
|
QRegularExpression re(R"(Поставщик[:\s]*(.+))");
|
||||||
|
if (auto m = re.match(line); m.hasMatch()) d.provider = m.captured(1).trimmed();
|
||||||
|
}
|
||||||
|
// "Счет отправителя: 9762***3258"
|
||||||
|
else if (line.contains(QString::fromUtf8("Счет отправителя")) ||
|
||||||
|
line.contains(QString::fromUtf8("чет отправителя"))) {
|
||||||
|
QRegularExpression re(R"((\d[\d*]+))");
|
||||||
|
if (auto m = re.match(line); m.hasMatch()) d.senderAccount = m.captured(1);
|
||||||
|
}
|
||||||
|
// "Счет получателя: 9762000186263942" или "992111100664"
|
||||||
|
else if (line.contains(QString::fromUtf8("Счет получателя")) ||
|
||||||
|
line.contains(QString::fromUtf8("чет получателя"))) {
|
||||||
|
QRegularExpression re(R"((\d{6,}))");
|
||||||
|
if (auto m = re.match(line); m.hasMatch()) d.receiverAccount = m.captured(1);
|
||||||
|
}
|
||||||
|
// "Сумма операции: 1.00"
|
||||||
|
else if (line.contains(QString::fromUtf8("Сумма операции")) ||
|
||||||
|
line.contains(QString::fromUtf8("умма операции"))) {
|
||||||
|
QRegularExpression re(R"((\d+[\.,]\d{2}))");
|
||||||
|
if (auto m = re.match(line); m.hasMatch()) {
|
||||||
|
QString val = m.captured(1);
|
||||||
|
val.replace(',', '.');
|
||||||
|
d.amount = val.toDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// "Комиссия: 0.00"
|
||||||
|
else if (line.contains(QString::fromUtf8("Комиссия")) ||
|
||||||
|
line.contains(QString::fromUtf8("омиссия"))) {
|
||||||
|
QRegularExpression re(R"((\d+[\.,]\d{2}))");
|
||||||
|
if (auto m = re.match(line); m.hasMatch()) {
|
||||||
|
QString val = m.captured(1);
|
||||||
|
val.replace(',', '.');
|
||||||
|
d.fee = val.toDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// "Статус: Успешный"
|
||||||
|
else if (line.contains(QString::fromUtf8("Статус"))) {
|
||||||
|
if (line.contains(QString::fromUtf8("Успешн"))) {
|
||||||
|
d.status = QString::fromUtf8("Успешный");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: если дата/время не найдены в "ключ: значение", ищем паттерном
|
||||||
|
if (d.date.isEmpty()) {
|
||||||
|
QRegularExpression re(R"((\d{2}\.\d{2}\.\d{4}))");
|
||||||
|
if (auto m = re.match(ocrText); m.hasMatch()) d.date = m.captured(1);
|
||||||
|
}
|
||||||
|
if (d.time.isEmpty()) {
|
||||||
|
QRegularExpression re(R"((\d{2}:\d{2}:\d{2}))");
|
||||||
|
auto it = re.globalMatch(ocrText);
|
||||||
|
QStringList times;
|
||||||
|
while (it.hasNext()) times.append(it.next().captured(1));
|
||||||
|
if (times.size() >= 2) d.time = times[1];
|
||||||
|
else if (times.size() == 1) d.time = times[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ocrText.contains(QString::fromUtf8("Успешн"))) d.status = QString::fromUtf8("Успешный");
|
||||||
|
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
namespace Dushanbe {
|
namespace Dushanbe {
|
||||||
|
|
||||||
GetLastTransactionsScript::GetLastTransactionsScript(
|
GetLastTransactionsScript::GetLastTransactionsScript(
|
||||||
@ -117,7 +330,7 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
// Ищем соответствующий node чтобы по нему тапнуть
|
// Ищем соответствующий node чтобы по нему тапнуть
|
||||||
Node txNode;
|
Node txNode;
|
||||||
for (const Node &node : xmlScreenParser.findAllNodes()) {
|
for (const Node &node : xmlScreenParser.findAllNodes()) {
|
||||||
if (node.className != "android.widget.ImageView" || !node.clickable) continue;
|
if (node.className != "android.widget.ImageView") continue;
|
||||||
if (!node.contentDesc.contains("***")) continue;
|
if (!node.contentDesc.contains("***")) continue;
|
||||||
// Проверяем что содержит нужную сумму
|
// Проверяем что содержит нужную сумму
|
||||||
QString amountStr = QString::number(tx.amount, 'f', 2);
|
QString amountStr = QString::number(tx.amount, 'f', 2);
|
||||||
@ -133,13 +346,14 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AdbUtils::makeTap(m_deviceId, txNode.x(), txNode.y());
|
AdbUtils::makeTap(m_deviceId, txNode.x(), txNode.y());
|
||||||
QThread::msleep(2000);
|
QThread::msleep(3000);
|
||||||
|
|
||||||
// Ждём экран деталей
|
// Ждём экран деталей — ищем кнопку "Назад" (признак экрана детали)
|
||||||
bool detailLoaded = false;
|
bool detailLoaded = 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.isTransactionDetailScreen()) {
|
if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Назад")).isEmpty() ||
|
||||||
|
!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Поделиться")).isEmpty()) {
|
||||||
detailLoaded = true;
|
detailLoaded = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -153,7 +367,18 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto detail = xmlScreenParser.parseTransactionDetail();
|
// OCR скриншота деталей
|
||||||
|
const QByteArray detailScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId);
|
||||||
|
const QString ocrText = recognizeTextFromImage(detailScreenshot);
|
||||||
|
qDebug() << "[Dushanbe::GetLastTransactions] OCR result:" << ocrText;
|
||||||
|
|
||||||
|
OcrTransactionDetail detail = parseDetailFromOcr(ocrText);
|
||||||
|
|
||||||
|
// Точечный OCR для номера операции (кроп + только цифры)
|
||||||
|
const QString preciseOpId = recognizeOperationId(detailScreenshot);
|
||||||
|
if (!preciseOpId.isEmpty()) {
|
||||||
|
detail.operationId = preciseOpId;
|
||||||
|
}
|
||||||
|
|
||||||
// Проверяем время ±1 час
|
// Проверяем время ±1 час
|
||||||
if (m_searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) {
|
if (m_searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) {
|
||||||
@ -193,7 +418,7 @@ void GetLastTransactionsScript::doStart() {
|
|||||||
saveTx.description = detail.provider;
|
saveTx.description = detail.provider;
|
||||||
saveTx.phone = detail.receiverAccount;
|
saveTx.phone = detail.receiverAccount;
|
||||||
saveTx.name = detail.receiverAccount;
|
saveTx.name = detail.receiverAccount;
|
||||||
saveTx.status = (detail.status == QString::fromUtf8("Успешный"))
|
saveTx.status = detail.status.contains(QString::fromUtf8("Успешн"))
|
||||||
? TransactionStatus::Complete : TransactionStatus::Unknown;
|
? TransactionStatus::Complete : TransactionStatus::Unknown;
|
||||||
saveTx.type = TransactionType::Phone;
|
saveTx.type = TransactionType::Phone;
|
||||||
saveTx.timestamp = QDateTime::currentDateTimeUtc();
|
saveTx.timestamp = QDateTime::currentDateTimeUtc();
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
|
||||||
#include "adb/AdbUtils.h"
|
#include "adb/AdbUtils.h"
|
||||||
@ -96,7 +97,8 @@ void GetProfileInfoScript::doStart() {
|
|||||||
|
|
||||||
// Парсим ФИО и телефон из бокового меню
|
// Парсим ФИО и телефон из бокового меню
|
||||||
const QString fullName = xmlScreenParser.parseProfileFullName();
|
const QString fullName = xmlScreenParser.parseProfileFullName();
|
||||||
const QString phone = xmlScreenParser.parseProfilePhone();
|
QString phone = xmlScreenParser.parseProfilePhone();
|
||||||
|
phone.remove(QRegularExpression("[^0-9+]"));
|
||||||
qDebug() << "[Dushanbe::GetProfileInfo] fullName:" << fullName << "phone:" << phone;
|
qDebug() << "[Dushanbe::GetProfileInfo] fullName:" << fullName << "phone:" << phone;
|
||||||
|
|
||||||
if (!fullName.isEmpty()) {
|
if (!fullName.isEmpty()) {
|
||||||
|
|||||||
488
android/dushanbe/PayByCardScript.cpp
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
#include "PayByCardScript.h"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include <tesseract/baseapi.h>
|
||||||
|
#include <leptonica/allheaders.h>
|
||||||
|
|
||||||
|
#include "adb/AdbUtils.h"
|
||||||
|
#include "dao/BankProfileDAO.h"
|
||||||
|
#include "dao/DeviceDAO.h"
|
||||||
|
#include "dao/TransactionDAO.h"
|
||||||
|
#include "AppLogger.h"
|
||||||
|
|
||||||
|
// Точечный OCR: находит строку "операции" и кропает правую часть для digit-only OCR
|
||||||
|
static QString recognizeOperationId(const QByteArray &pngData) {
|
||||||
|
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
|
||||||
|
static_cast<size_t>(pngData.size()));
|
||||||
|
if (!pix) return {};
|
||||||
|
|
||||||
|
const int w = pixGetWidth(pix);
|
||||||
|
const int h = pixGetHeight(pix);
|
||||||
|
|
||||||
|
auto *api = new tesseract::TessBaseAPI();
|
||||||
|
if (api->Init(nullptr, "rus+eng")) {
|
||||||
|
pixDestroy(&pix); delete api;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
api->SetImage(pix);
|
||||||
|
api->Recognize(nullptr);
|
||||||
|
|
||||||
|
int opLineY = -1, opLineH = 0;
|
||||||
|
bool prevWasNomer = false;
|
||||||
|
tesseract::ResultIterator *ri = api->GetIterator();
|
||||||
|
if (ri) {
|
||||||
|
do {
|
||||||
|
const char *word = ri->GetUTF8Text(tesseract::RIL_WORD);
|
||||||
|
if (word) {
|
||||||
|
const QString w_str = QString::fromUtf8(word);
|
||||||
|
delete[] word;
|
||||||
|
// Ищем "Номер" (или OCR-артефакты: "Homep", "Номep")
|
||||||
|
if (w_str.contains(QString::fromUtf8("омер")) ||
|
||||||
|
w_str.contains(QString::fromUtf8("omep"))) {
|
||||||
|
prevWasNomer = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// После "Номер" ищем "операции"
|
||||||
|
if (prevWasNomer &&
|
||||||
|
(w_str.contains(QString::fromUtf8("операции")) ||
|
||||||
|
w_str.contains(QString::fromUtf8("onepaции")))) {
|
||||||
|
int x1, y1, x2, y2;
|
||||||
|
ri->BoundingBox(tesseract::RIL_WORD, &x1, &y1, &x2, &y2);
|
||||||
|
opLineY = y1;
|
||||||
|
opLineH = y2 - y1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prevWasNomer = false;
|
||||||
|
}
|
||||||
|
} while (ri->Next(tesseract::RIL_WORD));
|
||||||
|
}
|
||||||
|
api->End();
|
||||||
|
delete api;
|
||||||
|
|
||||||
|
if (opLineY < 0) { pixDestroy(&pix); return {}; }
|
||||||
|
|
||||||
|
const int cropY = qMax(0, opLineY - opLineH / 2);
|
||||||
|
const int cropH = opLineH * 2;
|
||||||
|
BOX *box = boxCreate(w / 2, cropY, w / 2, qMin(cropH, h - cropY));
|
||||||
|
Pix *cropped = pixClipRectangle(pix, box, nullptr);
|
||||||
|
boxDestroy(&box);
|
||||||
|
pixDestroy(&pix);
|
||||||
|
if (!cropped) return {};
|
||||||
|
|
||||||
|
auto *api2 = new tesseract::TessBaseAPI();
|
||||||
|
if (api2->Init(nullptr, "eng")) {
|
||||||
|
pixDestroy(&cropped); delete api2;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
api2->SetVariable("tessedit_char_whitelist", "0123456789");
|
||||||
|
api2->SetPageSegMode(tesseract::PSM_SINGLE_LINE);
|
||||||
|
api2->SetImage(cropped);
|
||||||
|
char *text = api2->GetUTF8Text();
|
||||||
|
QString result = QString::fromUtf8(text).trimmed();
|
||||||
|
delete[] text;
|
||||||
|
pixDestroy(&cropped);
|
||||||
|
api2->End();
|
||||||
|
delete api2;
|
||||||
|
|
||||||
|
result.remove(QRegularExpression("[^0-9]"));
|
||||||
|
if (result.length() >= 8) {
|
||||||
|
qDebug() << "[Dushanbe::OCR] Precise operation ID:" << result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
PayByCardScript::PayByCardScript(
|
||||||
|
MaterialInfo account,
|
||||||
|
QString cardNumber,
|
||||||
|
double amount,
|
||||||
|
QObject *parent
|
||||||
|
) : CommonScript(parent),
|
||||||
|
m_account(std::move(account)),
|
||||||
|
m_cardNumber(std::move(cardNumber)),
|
||||||
|
m_amount(amount) {
|
||||||
|
}
|
||||||
|
|
||||||
|
PayByCardScript::~PayByCardScript() = default;
|
||||||
|
|
||||||
|
void PayByCardScript::doStart() {
|
||||||
|
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
|
||||||
|
const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode);
|
||||||
|
|
||||||
|
if (device.id.isEmpty() || app.id == -1) {
|
||||||
|
m_error = "Device or app not found: " + m_account.deviceId;
|
||||||
|
qCritical() << m_error;
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
|
||||||
|
|
||||||
|
if (!xmlScreenParser.isHomeScreen()) {
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Not on home screen, restarting app...";
|
||||||
|
if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode,
|
||||||
|
device.screenWidth, device.screenHeight)) {
|
||||||
|
m_error = "Cannot reach home screen: " + device.id;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByCard", device.id, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(device.adbSerial),
|
||||||
|
"CREATE_TRANSACTION");
|
||||||
|
AdbUtils::tryToKillApplication(device.adbSerial, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PayByCardScript::makePayment(
|
||||||
|
const QString &deviceId,
|
||||||
|
const QString &pinCode,
|
||||||
|
const int width,
|
||||||
|
const int height
|
||||||
|
) {
|
||||||
|
// 1. Тапаем "Переводы" в нижней навигации
|
||||||
|
Node transfersTab = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Переводы"));
|
||||||
|
if (transfersTab.isEmpty()) {
|
||||||
|
m_error = "Tab 'Переводы' not found on home screen";
|
||||||
|
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Tapping 'Переводы' tab";
|
||||||
|
AdbUtils::makeTap(deviceId, transfersTab.x(), transfersTab.y());
|
||||||
|
QThread::msleep(2000);
|
||||||
|
|
||||||
|
// 2. Ждём экран переводов — ищем "DC (по номеру карты)"
|
||||||
|
bool transfersLoaded = false;
|
||||||
|
Node cardTransferBtn;
|
||||||
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
cardTransferBtn = xmlScreenParser.findNodeByContentDesc(
|
||||||
|
QString::fromUtf8("DC (по номеру карты)"), true);
|
||||||
|
if (!cardTransferBtn.isEmpty()) {
|
||||||
|
transfersLoaded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Waiting for transfers screen, attempt" << attempt + 1;
|
||||||
|
QThread::msleep(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transfersLoaded) {
|
||||||
|
m_error = "Transfers screen not loaded or 'DC (по номеру карты)' not found";
|
||||||
|
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Тапаем "DC (по номеру карты)"
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Tapping 'DC (по номеру карты)'";
|
||||||
|
AdbUtils::makeTap(deviceId, cardTransferBtn.x(), cardTransferBtn.y());
|
||||||
|
QThread::msleep(2000);
|
||||||
|
|
||||||
|
// 4. Ждём экран ввода — ищем EditText с hint "Номер карты получателя"
|
||||||
|
bool inputScreenLoaded = false;
|
||||||
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node editText = xmlScreenParser.findEditTextByHint(
|
||||||
|
QString::fromUtf8("Номер карты получателя"));
|
||||||
|
if (editText.isEmpty()) {
|
||||||
|
editText = xmlScreenParser.findFirstEditText();
|
||||||
|
}
|
||||||
|
if (!editText.isEmpty()) {
|
||||||
|
inputScreenLoaded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Waiting for card input screen, attempt" << attempt + 1;
|
||||||
|
QThread::msleep(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputScreenLoaded) {
|
||||||
|
m_error = "Card input screen not loaded";
|
||||||
|
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Вводим номер карты в поле "Номер карты получателя"
|
||||||
|
Node cardEditText = xmlScreenParser.findEditTextByHint(
|
||||||
|
QString::fromUtf8("Номер карты получателя"));
|
||||||
|
if (cardEditText.isEmpty()) {
|
||||||
|
cardEditText = xmlScreenParser.findFirstEditText();
|
||||||
|
}
|
||||||
|
AdbUtils::makeTap(deviceId, cardEditText.x(), cardEditText.y());
|
||||||
|
QThread::msleep(500);
|
||||||
|
AdbUtils::inputText(deviceId, m_cardNumber);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
|
||||||
|
// Скрываем клавиатуру чтобы поле суммы стало видимым
|
||||||
|
AdbUtils::goBack(deviceId);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
|
||||||
|
// 6. Вводим сумму в поле "Сумма" (hint содержит "Сумма")
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node sumEditText = xmlScreenParser.findEditTextByHint(QString::fromUtf8("Сумма"));
|
||||||
|
if (sumEditText.isEmpty()) {
|
||||||
|
m_error = "Sum input field not found";
|
||||||
|
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdbUtils::makeTap(deviceId, sumEditText.x(), sumEditText.y());
|
||||||
|
QThread::msleep(500);
|
||||||
|
|
||||||
|
// Сумма приходит с копейками (например 100.50), форматируем как целое или с точкой
|
||||||
|
const QString amountStr = (m_amount == static_cast<int>(m_amount))
|
||||||
|
? QString::number(static_cast<int>(m_amount))
|
||||||
|
: QString::number(m_amount, 'f', 2);
|
||||||
|
AdbUtils::inputText(deviceId, amountStr);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
|
||||||
|
// 7. Скрываем клавиатуру и нажимаем "Далее"
|
||||||
|
AdbUtils::goBack(deviceId);
|
||||||
|
QThread::msleep(500);
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее"));
|
||||||
|
if (daleeBtn.isEmpty()) {
|
||||||
|
daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false);
|
||||||
|
}
|
||||||
|
if (daleeBtn.isEmpty()) {
|
||||||
|
m_error = "Button 'Далее' not found";
|
||||||
|
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Tapping 'Далее'";
|
||||||
|
AdbUtils::makeTap(deviceId, daleeBtn.x(), daleeBtn.y());
|
||||||
|
QThread::msleep(2000);
|
||||||
|
|
||||||
|
// 8. После "Далее" кнопка меняется на "Оплатить" — ждём и нажимаем
|
||||||
|
bool oplatitFound = false;
|
||||||
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node oplatitBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Оплатить"));
|
||||||
|
if (oplatitBtn.isEmpty()) {
|
||||||
|
oplatitBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Оплатить"), false);
|
||||||
|
}
|
||||||
|
if (!oplatitBtn.isEmpty()) {
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Tapping 'Оплатить'";
|
||||||
|
AdbUtils::makeTap(deviceId, oplatitBtn.x(), oplatitBtn.y());
|
||||||
|
oplatitFound = true;
|
||||||
|
QThread::msleep(3000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Waiting for 'Оплатить', attempt" << attempt + 1;
|
||||||
|
QThread::msleep(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oplatitFound) {
|
||||||
|
m_error = "Button 'Оплатить' not found";
|
||||||
|
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Ждём bottom sheet "Подтверждение платежа" и тапаем "Подтверждаю"
|
||||||
|
// content-desc содержит "Подтверждение платежа...Подтверждаю"
|
||||||
|
// Кнопка "Подтверждаю" визуально внизу bottom sheet
|
||||||
|
bool confirmed = false;
|
||||||
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
|
||||||
|
// Пин-код может появиться
|
||||||
|
if (xmlScreenParser.isPinCodeScreen()) {
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] PIN code screen, entering PIN...";
|
||||||
|
inputPinCode(deviceId, pinCode);
|
||||||
|
QThread::msleep(3000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем элемент с "Подтверждение платежа" и "Подтверждаю" в content-desc
|
||||||
|
Node confirmNode = xmlScreenParser.findNodeByContentDesc(
|
||||||
|
QString::fromUtf8("Подтверждение платежа"), true);
|
||||||
|
if (!confirmNode.isEmpty() &&
|
||||||
|
confirmNode.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) {
|
||||||
|
// Тапаем внизу этого элемента (кнопка "Подтверждаю" в нижней части)
|
||||||
|
// Кнопка "Подтверждаю" всегда внизу bottom sheet — тапаем по 92% высоты экрана
|
||||||
|
const int tapY = height * 92 / 100;
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Tapping 'Подтверждаю' at y=" << tapY;
|
||||||
|
AdbUtils::makeTap(deviceId, confirmNode.x(), tapY);
|
||||||
|
confirmed = true;
|
||||||
|
QThread::msleep(4000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Waiting for confirmation sheet, attempt" << attempt + 1;
|
||||||
|
QThread::msleep(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
m_error = "Confirmation sheet not found";
|
||||||
|
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Ждём результат — "Платеж выполнен" или ошибку
|
||||||
|
bool success = false;
|
||||||
|
for (int attempt = 0; attempt < 15; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
|
||||||
|
// Пин-код
|
||||||
|
if (xmlScreenParser.isPinCodeScreen()) {
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] PIN code screen (retry), entering PIN...";
|
||||||
|
inputPinCode(deviceId, pinCode);
|
||||||
|
QThread::msleep(3000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Успех: content-desc начинается с "Платеж выполнен"
|
||||||
|
if (!xmlScreenParser.findNodeByContentDesc(
|
||||||
|
QString::fromUtf8("Платеж выполнен"), true).isEmpty()) {
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ошибка
|
||||||
|
if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка"), true).isEmpty() ||
|
||||||
|
!xmlScreenParser.findTextNode(QString::fromUtf8("Ошибка")).isEmpty()) {
|
||||||
|
m_error = "Transfer failed: error screen detected";
|
||||||
|
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Waiting for result, attempt" << attempt + 1;
|
||||||
|
QThread::msleep(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
m_error = "Payment result not received within timeout";
|
||||||
|
qWarning() << "[Dushanbe::PayByCard]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Payment successful!";
|
||||||
|
m_error.clear();
|
||||||
|
|
||||||
|
// Скриншот чека
|
||||||
|
const QByteArray receiptImage = AdbUtils::captureScreenshotBytes(deviceId);
|
||||||
|
|
||||||
|
// Парсим детали из content-desc "Платеж выполнен"
|
||||||
|
// Формат: "Платеж выполнен\nПоставщик:\n...\nКомиссия:\n0.00\nДата:\n05.04.26\nВремя:\n20:19:56\nНа главную"
|
||||||
|
QString bankTime;
|
||||||
|
double fee = 0;
|
||||||
|
|
||||||
|
Node resultNode = xmlScreenParser.findNodeByContentDesc(
|
||||||
|
QString::fromUtf8("Платеж выполнен"), true);
|
||||||
|
if (!resultNode.isEmpty()) {
|
||||||
|
const QStringList lines = resultNode.contentDesc.split('\n');
|
||||||
|
for (int i = 0; i < lines.size(); ++i) {
|
||||||
|
const QString line = lines[i].trimmed();
|
||||||
|
if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) {
|
||||||
|
fee = lines[i + 1].trimmed().toDouble();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Дата:")) && i + 1 < lines.size()) {
|
||||||
|
bankTime = lines[i + 1].trimmed();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Время:")) && i + 1 < lines.size()) {
|
||||||
|
bankTime += " " + lines[i + 1].trimmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Result: date=" << bankTime << "fee=" << fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переходим в историю для получения ID операции через OCR
|
||||||
|
// 1. Тапаем "На главную" (внизу экрана результата)
|
||||||
|
Node naGlavnuyuNode = xmlScreenParser.findNodeByContentDesc(
|
||||||
|
QString::fromUtf8("Платеж выполнен"), true);
|
||||||
|
if (!naGlavnuyuNode.isEmpty()) {
|
||||||
|
const int tapY = height * 92 / 100;
|
||||||
|
AdbUtils::makeTap(deviceId, width / 2, tapY);
|
||||||
|
QThread::msleep(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Тапаем "История" в нижней навигации
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node historyTab = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("История"));
|
||||||
|
if (!historyTab.isEmpty()) {
|
||||||
|
AdbUtils::makeTap(deviceId, historyTab.x(), historyTab.y());
|
||||||
|
QThread::msleep(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Тапаем первую транзакцию в списке (содержит m_cardNumber)
|
||||||
|
QString operationId;
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node txNode = xmlScreenParser.findNodeByContentDesc(m_cardNumber, true);
|
||||||
|
if (txNode.isEmpty()) {
|
||||||
|
// Fallback: ищем "Сегодня" — транзакция обычно сразу после
|
||||||
|
txNode = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Сегодня"), true);
|
||||||
|
}
|
||||||
|
if (!txNode.isEmpty()) {
|
||||||
|
AdbUtils::makeTap(deviceId, txNode.x(), txNode.y());
|
||||||
|
QThread::msleep(3000);
|
||||||
|
|
||||||
|
// 4. Делаем скриншот детали и OCR
|
||||||
|
const QByteArray detailScreenshot = AdbUtils::captureScreenshotBytes(deviceId);
|
||||||
|
if (!detailScreenshot.isEmpty()) {
|
||||||
|
operationId = recognizeOperationId(detailScreenshot);
|
||||||
|
if (!operationId.isEmpty()) {
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Operation ID:" << operationId;
|
||||||
|
} else {
|
||||||
|
qWarning() << "[Dushanbe::PayByCard] Could not extract operation ID";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Назад из деталей
|
||||||
|
AdbUtils::goBack(deviceId);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
} else {
|
||||||
|
qWarning() << "[Dushanbe::PayByCard] Transaction not found in history";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем транзакцию
|
||||||
|
TransactionInfo tx;
|
||||||
|
tx.accountId = m_account.id;
|
||||||
|
tx.amount = -m_amount;
|
||||||
|
tx.fee = fee;
|
||||||
|
tx.status = TransactionStatus::Complete;
|
||||||
|
tx.type = TransactionType::Card;
|
||||||
|
tx.phone = m_cardNumber;
|
||||||
|
tx.bankName = "dushanbe_city_bank";
|
||||||
|
tx.bankTime = bankTime;
|
||||||
|
tx.bankTrExternalId = operationId;
|
||||||
|
tx.timestamp = QDateTime::currentDateTimeUtc();
|
||||||
|
tx.completeTime = QDateTime::currentDateTimeUtc();
|
||||||
|
tx.receiptImage = receiptImage;
|
||||||
|
|
||||||
|
const int txId = TransactionDAO::insertTransaction(tx);
|
||||||
|
if (txId != -1) {
|
||||||
|
qDebug() << "[Dushanbe::PayByCard] Transaction saved, id:" << txId
|
||||||
|
<< "operationId:" << operationId;
|
||||||
|
if (!receiptImage.isEmpty()) {
|
||||||
|
TransactionDAO::updateReceiptImage(txId, receiptImage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qWarning() << "[Dushanbe::PayByCard] Failed to save transaction";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
36
android/dushanbe/PayByCardScript.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "CommonScript.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
class PayByCardScript final : public CommonScript {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
PayByCardScript(
|
||||||
|
MaterialInfo account,
|
||||||
|
QString cardNumber,
|
||||||
|
double amount,
|
||||||
|
QObject *parent = nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
~PayByCardScript() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void doStart() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const MaterialInfo m_account;
|
||||||
|
const QString m_cardNumber;
|
||||||
|
const double m_amount;
|
||||||
|
QString m_error;
|
||||||
|
|
||||||
|
void makePayment(
|
||||||
|
const QString &deviceId,
|
||||||
|
const QString &pinCode,
|
||||||
|
int width,
|
||||||
|
int height
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
473
android/dushanbe/PayByPhoneScript.cpp
Normal file
@ -0,0 +1,473 @@
|
|||||||
|
#include "PayByPhoneScript.h"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include <tesseract/baseapi.h>
|
||||||
|
#include <leptonica/allheaders.h>
|
||||||
|
|
||||||
|
#include "adb/AdbUtils.h"
|
||||||
|
#include "dao/BankProfileDAO.h"
|
||||||
|
#include "dao/DeviceDAO.h"
|
||||||
|
#include "dao/TransactionDAO.h"
|
||||||
|
#include "AppLogger.h"
|
||||||
|
|
||||||
|
static QString recognizeOperationId(const QByteArray &pngData) {
|
||||||
|
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
|
||||||
|
static_cast<size_t>(pngData.size()));
|
||||||
|
if (!pix) return {};
|
||||||
|
|
||||||
|
const int w = pixGetWidth(pix);
|
||||||
|
const int h = pixGetHeight(pix);
|
||||||
|
|
||||||
|
auto *api = new tesseract::TessBaseAPI();
|
||||||
|
if (api->Init(nullptr, "rus+eng")) {
|
||||||
|
pixDestroy(&pix); delete api;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
api->SetImage(pix);
|
||||||
|
api->Recognize(nullptr);
|
||||||
|
|
||||||
|
int opLineY = -1, opLineH = 0;
|
||||||
|
bool prevWasNomer = false;
|
||||||
|
tesseract::ResultIterator *ri = api->GetIterator();
|
||||||
|
if (ri) {
|
||||||
|
do {
|
||||||
|
const char *word = ri->GetUTF8Text(tesseract::RIL_WORD);
|
||||||
|
if (word) {
|
||||||
|
const QString w_str = QString::fromUtf8(word);
|
||||||
|
delete[] word;
|
||||||
|
if (w_str.contains(QString::fromUtf8("омер")) ||
|
||||||
|
w_str.contains(QString::fromUtf8("omep"))) {
|
||||||
|
prevWasNomer = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (prevWasNomer &&
|
||||||
|
(w_str.contains(QString::fromUtf8("операции")) ||
|
||||||
|
w_str.contains(QString::fromUtf8("onepaции")))) {
|
||||||
|
int x1, y1, x2, y2;
|
||||||
|
ri->BoundingBox(tesseract::RIL_WORD, &x1, &y1, &x2, &y2);
|
||||||
|
opLineY = y1;
|
||||||
|
opLineH = y2 - y1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prevWasNomer = false;
|
||||||
|
}
|
||||||
|
} while (ri->Next(tesseract::RIL_WORD));
|
||||||
|
}
|
||||||
|
api->End();
|
||||||
|
delete api;
|
||||||
|
|
||||||
|
if (opLineY < 0) { pixDestroy(&pix); return {}; }
|
||||||
|
|
||||||
|
const int cropY = qMax(0, opLineY - opLineH / 2);
|
||||||
|
const int cropH = opLineH * 2;
|
||||||
|
BOX *box = boxCreate(w / 2, cropY, w / 2, qMin(cropH, h - cropY));
|
||||||
|
Pix *cropped = pixClipRectangle(pix, box, nullptr);
|
||||||
|
boxDestroy(&box);
|
||||||
|
pixDestroy(&pix);
|
||||||
|
if (!cropped) return {};
|
||||||
|
|
||||||
|
auto *api2 = new tesseract::TessBaseAPI();
|
||||||
|
if (api2->Init(nullptr, "eng")) {
|
||||||
|
pixDestroy(&cropped); delete api2;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
api2->SetVariable("tessedit_char_whitelist", "0123456789");
|
||||||
|
api2->SetPageSegMode(tesseract::PSM_SINGLE_LINE);
|
||||||
|
api2->SetImage(cropped);
|
||||||
|
char *text = api2->GetUTF8Text();
|
||||||
|
QString result = QString::fromUtf8(text).trimmed();
|
||||||
|
delete[] text;
|
||||||
|
pixDestroy(&cropped);
|
||||||
|
api2->End();
|
||||||
|
delete api2;
|
||||||
|
|
||||||
|
result.remove(QRegularExpression("[^0-9]"));
|
||||||
|
if (result.length() >= 8) {
|
||||||
|
qDebug() << "[Dushanbe::OCR] Precise operation ID:" << result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
PayByPhoneScript::PayByPhoneScript(
|
||||||
|
MaterialInfo account,
|
||||||
|
QString phone,
|
||||||
|
double amount,
|
||||||
|
QObject *parent
|
||||||
|
) : CommonScript(parent),
|
||||||
|
m_account(std::move(account)),
|
||||||
|
m_phone(std::move(phone)),
|
||||||
|
m_amount(amount) {
|
||||||
|
}
|
||||||
|
|
||||||
|
PayByPhoneScript::~PayByPhoneScript() = default;
|
||||||
|
|
||||||
|
void PayByPhoneScript::doStart() {
|
||||||
|
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
|
||||||
|
const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode);
|
||||||
|
|
||||||
|
if (device.id.isEmpty() || app.id == -1) {
|
||||||
|
m_error = "Device or app not found: " + m_account.deviceId;
|
||||||
|
qCritical() << m_error;
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
|
||||||
|
|
||||||
|
if (!xmlScreenParser.isHomeScreen()) {
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Not on home screen, restarting app...";
|
||||||
|
if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode,
|
||||||
|
device.screenWidth, device.screenHeight)) {
|
||||||
|
m_error = "Cannot reach home screen: " + device.id;
|
||||||
|
qWarning() << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByPhone", device.id, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(device.adbSerial),
|
||||||
|
"CREATE_TRANSACTION");
|
||||||
|
AdbUtils::tryToKillApplication(device.adbSerial, app.package);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight);
|
||||||
|
emit finishedWithResult(m_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PayByPhoneScript::makePayment(
|
||||||
|
const QString &deviceId,
|
||||||
|
const QString &pinCode,
|
||||||
|
const int width,
|
||||||
|
const int height
|
||||||
|
) {
|
||||||
|
// 1. Тапаем "Переводы" в нижней навигации
|
||||||
|
Node transfersTab = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Переводы"));
|
||||||
|
if (transfersTab.isEmpty()) {
|
||||||
|
m_error = "Tab 'Переводы' not found on home screen";
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Tapping 'Переводы' tab";
|
||||||
|
AdbUtils::makeTap(deviceId, transfersTab.x(), transfersTab.y());
|
||||||
|
QThread::msleep(2000);
|
||||||
|
|
||||||
|
// 2. Ждём экран переводов — ищем "DC (по номеру телефона)"
|
||||||
|
bool transfersLoaded = false;
|
||||||
|
Node phoneTransferBtn;
|
||||||
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
phoneTransferBtn = xmlScreenParser.findNodeByContentDesc(
|
||||||
|
QString::fromUtf8("DC (по номеру телефона)"), true);
|
||||||
|
if (!phoneTransferBtn.isEmpty()) {
|
||||||
|
transfersLoaded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Waiting for transfers screen, attempt" << attempt + 1;
|
||||||
|
QThread::msleep(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transfersLoaded) {
|
||||||
|
m_error = "Transfers screen not loaded or 'DC (по номеру телефона)' not found";
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Тапаем "DC (по номеру телефона)"
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Tapping 'DC (по номеру телефона)'";
|
||||||
|
AdbUtils::makeTap(deviceId, phoneTransferBtn.x(), phoneTransferBtn.y());
|
||||||
|
QThread::msleep(2000);
|
||||||
|
|
||||||
|
// 4. Ждём экран ввода — ищем EditText с hint "Номер телефона получателя"
|
||||||
|
bool inputScreenLoaded = false;
|
||||||
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node editText = xmlScreenParser.findEditTextByHint(
|
||||||
|
QString::fromUtf8("Номер телефона получателя"));
|
||||||
|
if (editText.isEmpty()) {
|
||||||
|
editText = xmlScreenParser.findFirstEditText();
|
||||||
|
}
|
||||||
|
if (!editText.isEmpty()) {
|
||||||
|
inputScreenLoaded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Waiting for phone input screen, attempt" << attempt + 1;
|
||||||
|
QThread::msleep(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputScreenLoaded) {
|
||||||
|
m_error = "Phone input screen not loaded";
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Вводим номер телефона
|
||||||
|
Node phoneEditText = xmlScreenParser.findEditTextByHint(
|
||||||
|
QString::fromUtf8("Номер телефона получателя"));
|
||||||
|
if (phoneEditText.isEmpty()) {
|
||||||
|
phoneEditText = xmlScreenParser.findFirstEditText();
|
||||||
|
}
|
||||||
|
AdbUtils::makeTap(deviceId, phoneEditText.x(), phoneEditText.y());
|
||||||
|
QThread::msleep(500);
|
||||||
|
|
||||||
|
// Убираем "+" из номера если есть — вводим только цифры
|
||||||
|
QString phoneDigits = m_phone;
|
||||||
|
phoneDigits.remove('+');
|
||||||
|
AdbUtils::inputText(deviceId, phoneDigits);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
|
||||||
|
// Скрываем клавиатуру чтобы поле суммы стало видимым
|
||||||
|
AdbUtils::goBack(deviceId);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
|
||||||
|
// 6. Вводим сумму в поле "Сумма"
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node sumEditText = xmlScreenParser.findEditTextByHint(QString::fromUtf8("Сумма"));
|
||||||
|
if (sumEditText.isEmpty()) {
|
||||||
|
m_error = "Sum input field not found";
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdbUtils::makeTap(deviceId, sumEditText.x(), sumEditText.y());
|
||||||
|
QThread::msleep(500);
|
||||||
|
|
||||||
|
const QString amountStr = (m_amount == static_cast<int>(m_amount))
|
||||||
|
? QString::number(static_cast<int>(m_amount))
|
||||||
|
: QString::number(m_amount, 'f', 2);
|
||||||
|
AdbUtils::inputText(deviceId, amountStr);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
|
||||||
|
// 7. Скрываем клавиатуру и нажимаем "Далее"
|
||||||
|
AdbUtils::goBack(deviceId);
|
||||||
|
QThread::msleep(500);
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее"));
|
||||||
|
if (daleeBtn.isEmpty()) {
|
||||||
|
daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false);
|
||||||
|
}
|
||||||
|
if (daleeBtn.isEmpty()) {
|
||||||
|
m_error = "Button 'Далее' not found";
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Tapping 'Далее'";
|
||||||
|
AdbUtils::makeTap(deviceId, daleeBtn.x(), daleeBtn.y());
|
||||||
|
QThread::msleep(2000);
|
||||||
|
|
||||||
|
// 8. После "Далее" кнопка меняется на "Оплатить"
|
||||||
|
bool oplatitFound = false;
|
||||||
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node oplatitBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Оплатить"));
|
||||||
|
if (oplatitBtn.isEmpty()) {
|
||||||
|
oplatitBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Оплатить"), false);
|
||||||
|
}
|
||||||
|
if (!oplatitBtn.isEmpty()) {
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Tapping 'Оплатить'";
|
||||||
|
AdbUtils::makeTap(deviceId, oplatitBtn.x(), oplatitBtn.y());
|
||||||
|
oplatitFound = true;
|
||||||
|
QThread::msleep(3000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Waiting for 'Оплатить', attempt" << attempt + 1;
|
||||||
|
QThread::msleep(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oplatitFound) {
|
||||||
|
m_error = "Button 'Оплатить' not found";
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Ждём bottom sheet "Подтверждение платежа" и тапаем "Подтверждаю"
|
||||||
|
bool confirmed = false;
|
||||||
|
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
|
||||||
|
if (xmlScreenParser.isPinCodeScreen()) {
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] PIN code screen, entering PIN...";
|
||||||
|
inputPinCode(deviceId, pinCode);
|
||||||
|
QThread::msleep(3000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node confirmNode = xmlScreenParser.findNodeByContentDesc(
|
||||||
|
QString::fromUtf8("Подтверждение платежа"), true);
|
||||||
|
if (!confirmNode.isEmpty() &&
|
||||||
|
confirmNode.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) {
|
||||||
|
// Кнопка "Подтверждаю" всегда внизу bottom sheet — тапаем по 92% высоты экрана
|
||||||
|
const int tapY = height * 92 / 100;
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Tapping 'Подтверждаю' at y=" << tapY;
|
||||||
|
AdbUtils::makeTap(deviceId, confirmNode.x(), tapY);
|
||||||
|
confirmed = true;
|
||||||
|
QThread::msleep(4000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Waiting for confirmation sheet, attempt" << attempt + 1;
|
||||||
|
QThread::msleep(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
m_error = "Confirmation sheet not found";
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Ждём результат — "Платеж выполнен" или ошибку
|
||||||
|
bool success = false;
|
||||||
|
for (int attempt = 0; attempt < 15; ++attempt) {
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
|
||||||
|
if (xmlScreenParser.isPinCodeScreen()) {
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] PIN code screen (retry), entering PIN...";
|
||||||
|
inputPinCode(deviceId, pinCode);
|
||||||
|
QThread::msleep(3000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!xmlScreenParser.findNodeByContentDesc(
|
||||||
|
QString::fromUtf8("Платеж выполнен"), true).isEmpty()) {
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка"), true).isEmpty() ||
|
||||||
|
!xmlScreenParser.findTextNode(QString::fromUtf8("Ошибка")).isEmpty()) {
|
||||||
|
m_error = "Transfer failed: error screen detected";
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Waiting for result, attempt" << attempt + 1;
|
||||||
|
QThread::msleep(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
m_error = "Payment result not received within timeout";
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
|
||||||
|
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
|
||||||
|
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Payment successful!";
|
||||||
|
m_error.clear();
|
||||||
|
|
||||||
|
// Скриншот чека
|
||||||
|
const QByteArray receiptImage = AdbUtils::captureScreenshotBytes(deviceId);
|
||||||
|
|
||||||
|
// Парсим детали из content-desc "Платеж выполнен"
|
||||||
|
QString bankTime;
|
||||||
|
double fee = 0;
|
||||||
|
|
||||||
|
Node resultNode = xmlScreenParser.findNodeByContentDesc(
|
||||||
|
QString::fromUtf8("Платеж выполнен"), true);
|
||||||
|
if (!resultNode.isEmpty()) {
|
||||||
|
const QStringList lines = resultNode.contentDesc.split('\n');
|
||||||
|
for (int i = 0; i < lines.size(); ++i) {
|
||||||
|
const QString line = lines[i].trimmed();
|
||||||
|
if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) {
|
||||||
|
fee = lines[i + 1].trimmed().toDouble();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Дата:")) && i + 1 < lines.size()) {
|
||||||
|
bankTime = lines[i + 1].trimmed();
|
||||||
|
} else if (line.startsWith(QString::fromUtf8("Время:")) && i + 1 < lines.size()) {
|
||||||
|
bankTime += " " + lines[i + 1].trimmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Result: date=" << bankTime << "fee=" << fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переходим в историю для получения ID операции через OCR
|
||||||
|
Node naGlavnuyuNode = xmlScreenParser.findNodeByContentDesc(
|
||||||
|
QString::fromUtf8("Платеж выполнен"), true);
|
||||||
|
if (!naGlavnuyuNode.isEmpty()) {
|
||||||
|
const int tapY = height * 92 / 100;
|
||||||
|
AdbUtils::makeTap(deviceId, width / 2, tapY);
|
||||||
|
QThread::msleep(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node historyTab = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("История"));
|
||||||
|
if (!historyTab.isEmpty()) {
|
||||||
|
AdbUtils::makeTap(deviceId, historyTab.x(), historyTab.y());
|
||||||
|
QThread::msleep(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString operationId;
|
||||||
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||||
|
Node txNode = xmlScreenParser.findNodeByContentDesc(phoneDigits, true);
|
||||||
|
if (txNode.isEmpty()) {
|
||||||
|
txNode = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Сегодня"), true);
|
||||||
|
}
|
||||||
|
if (!txNode.isEmpty()) {
|
||||||
|
AdbUtils::makeTap(deviceId, txNode.x(), txNode.y());
|
||||||
|
QThread::msleep(3000);
|
||||||
|
|
||||||
|
const QByteArray detailScreenshot = AdbUtils::captureScreenshotBytes(deviceId);
|
||||||
|
if (!detailScreenshot.isEmpty()) {
|
||||||
|
operationId = recognizeOperationId(detailScreenshot);
|
||||||
|
if (!operationId.isEmpty()) {
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Operation ID:" << operationId;
|
||||||
|
} else {
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone] Could not extract operation ID";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AdbUtils::goBack(deviceId);
|
||||||
|
QThread::msleep(1000);
|
||||||
|
} else {
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone] Transaction not found in history";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем транзакцию
|
||||||
|
TransactionInfo tx;
|
||||||
|
tx.accountId = m_account.id;
|
||||||
|
tx.amount = -m_amount;
|
||||||
|
tx.fee = fee;
|
||||||
|
tx.status = TransactionStatus::Complete;
|
||||||
|
tx.type = TransactionType::Phone;
|
||||||
|
tx.phone = m_phone;
|
||||||
|
tx.bankName = "dushanbe_city_bank";
|
||||||
|
tx.bankTime = bankTime;
|
||||||
|
tx.bankTrExternalId = operationId;
|
||||||
|
tx.timestamp = QDateTime::currentDateTimeUtc();
|
||||||
|
tx.completeTime = QDateTime::currentDateTimeUtc();
|
||||||
|
tx.receiptImage = receiptImage;
|
||||||
|
|
||||||
|
const int txId = TransactionDAO::insertTransaction(tx);
|
||||||
|
if (txId != -1) {
|
||||||
|
qDebug() << "[Dushanbe::PayByPhone] Transaction saved, id:" << txId
|
||||||
|
<< "operationId:" << operationId;
|
||||||
|
if (!receiptImage.isEmpty()) {
|
||||||
|
TransactionDAO::updateReceiptImage(txId, receiptImage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qWarning() << "[Dushanbe::PayByPhone] Failed to save transaction";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
36
android/dushanbe/PayByPhoneScript.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "CommonScript.h"
|
||||||
|
|
||||||
|
namespace Dushanbe {
|
||||||
|
|
||||||
|
class PayByPhoneScript final : public CommonScript {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
PayByPhoneScript(
|
||||||
|
MaterialInfo account,
|
||||||
|
QString phone,
|
||||||
|
double amount,
|
||||||
|
QObject *parent = nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
~PayByPhoneScript() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void doStart() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const MaterialInfo m_account;
|
||||||
|
const QString m_phone;
|
||||||
|
const double m_amount;
|
||||||
|
QString m_error;
|
||||||
|
|
||||||
|
void makePayment(
|
||||||
|
const QString &deviceId,
|
||||||
|
const QString &pinCode,
|
||||||
|
int width,
|
||||||
|
int height
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Dushanbe
|
||||||
@ -467,7 +467,7 @@ ScreenXmlParser::TransactionDetail ScreenXmlParser::parseTransactionDetail() {
|
|||||||
|
|
||||||
Node ScreenXmlParser::findEditTextByHint(const QString &hint) {
|
Node ScreenXmlParser::findEditTextByHint(const QString &hint) {
|
||||||
for (const Node &node : m_nodes) {
|
for (const Node &node : m_nodes) {
|
||||||
if (node.className == "android.widget.EditText" && node.hint == hint) {
|
if (node.className == "android.widget.EditText" && node.hint.contains(hint)) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
#include "GetProfileInfoScript.h"
|
#include "GetProfileInfoScript.h"
|
||||||
|
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
|||||||
BIN
assets/dushanbe/card_transfer_confirm.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
1
assets/dushanbe/card_transfer_confirm.xml
Normal file
BIN
assets/dushanbe/card_transfer_confirm_detail.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
1
assets/dushanbe/card_transfer_confirm_detail.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><hierarchy rotation="0"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="2" hint=""><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="true" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.widget.Button" package="tj.dc.next1" content-desc="Подтверждение платежа 1.00 TJS Поставщик: DC (по номеру карты) Номер/счет: 9762000186263942 Способ оплаты: 9762 **** **** 3258 Сумма зачисления: 1.00 TJS Комиссия: 0.00 TJS Сумма списания: 1.00 TJS Подтверждаю" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" bounds="[0,1020][1080,2400]" drawing-order="0" hint="" /></node></node><node index="1" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="Закрыть" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint="" /></node></node></node></node></node></node></node></hierarchy>UI hierchary dumped to: /dev/tty
|
||||||
BIN
assets/dushanbe/card_transfer_input.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
1
assets/dushanbe/card_transfer_input.xml
Normal file
BIN
assets/dushanbe/card_transfer_success.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
1
assets/dushanbe/card_transfer_success.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><hierarchy rotation="0"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="2" hint=""><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="true" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.widget.ImageView" package="tj.dc.next1" content-desc="Платеж выполнен Поставщик: DC (по номеру карты) Получатель: 9762000186263942 К зачислению: 1 Комиссия: 0.00 Дата: 05.04.26 Время: 20:19:56 На главную" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" bounds="[0,558][1080,2400]" drawing-order="0" hint="" /></node></node><node index="1" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="Закрыть" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint="" /></node></node></node></node></node></node></node></hierarchy>UI hierchary dumped to: /dev/tty
|
||||||
BIN
assets/dushanbe/card_transfer_tx_detail.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
1
assets/dushanbe/card_transfer_tx_detail.xml
Normal file
BIN
assets/dushanbe/phone_transfer_confirm.png
Normal file
|
After Width: | Height: | Size: 203 KiB |
1
assets/dushanbe/phone_transfer_confirm.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><hierarchy rotation="0"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="2" hint=""><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="true" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.widget.Button" package="tj.dc.next1" content-desc="Подтверждение платежа 1.00 TJS Поставщик: DC (по номеру телефона) Номер/счет: 992111100664 Способ оплаты: 9762 **** **** 3258 Сумма зачисления: 1.00 TJS Комиссия: 0.00 TJS Сумма списания: 1.00 TJS Подтверждаю" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" bounds="[0,1020][1080,2400]" drawing-order="0" hint="" /></node></node><node index="1" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="Закрыть" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint="" /></node></node></node></node></node></node></node></hierarchy>UI hierchary dumped to: /dev/tty
|
||||||
BIN
assets/dushanbe/phone_transfer_input.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
1
assets/dushanbe/phone_transfer_input.xml
Normal file
BIN
assets/dushanbe/phone_transfer_success.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
1
assets/dushanbe/phone_transfer_success.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><hierarchy rotation="0"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="2" hint=""><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="true" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint=""><node index="0" text="" resource-id="" class="android.widget.ImageView" package="tj.dc.next1" content-desc="Платеж выполнен Поставщик: DC (по номеру телефона) Получатель: 992111100664 К зачислению: 1 Комиссия: 0.00 Дата: 05.04.26 Время: 20:36:19 На главную" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" bounds="[0,558][1080,2400]" drawing-order="0" hint="" /></node></node><node index="1" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="Закрыть" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="0" hint="" /></node></node></node></node></node></node></node></hierarchy>UI hierchary dumped to: /dev/tty
|
||||||
BIN
assets/dushanbe/transfers_screen.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
1
assets/dushanbe/transfers_screen.xml
Normal file
@ -1,5 +1,5 @@
|
|||||||
[apps]
|
[apps]
|
||||||
list=ozon, black, dushanbe
|
list=ozon, black, dushanbe_city_bank
|
||||||
|
|
||||||
[common]
|
[common]
|
||||||
currencies=USD, ARS, RUB, TJS
|
currencies=USD, ARS, RUB, TJS
|
||||||
@ -30,9 +30,8 @@ currency=RUB
|
|||||||
name=Ozon
|
name=Ozon
|
||||||
phone_code=7
|
phone_code=7
|
||||||
|
|
||||||
[dushanbe]
|
[dushanbe_city_bank]
|
||||||
android_package_name=tj.dc.next1
|
android_package_name=tj.dc.next1
|
||||||
currency=TJS
|
currency=TJS
|
||||||
name=Dushanbe City
|
name=Dushanbe City
|
||||||
phone_code=992
|
phone_code=992
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <QSqlError>
|
#include <QSqlError>
|
||||||
#include <QSqlQuery>
|
#include <QSqlQuery>
|
||||||
|
#include <QSqlRecord>
|
||||||
|
|
||||||
#include "DatabaseManager.h"
|
#include "DatabaseManager.h"
|
||||||
#include "time/DateUtils.h"
|
#include "time/DateUtils.h"
|
||||||
@ -23,6 +24,10 @@ EventInfo parseEvent(const QSqlQuery &query) {
|
|||||||
event.timestamp = DateUtils::fromUtcString(query.value("timestamp").toString());
|
event.timestamp = DateUtils::fromUtcString(query.value("timestamp").toString());
|
||||||
event.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());
|
event.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());
|
||||||
event.completeTime = DateUtils::fromUtcString(query.value("complete_time").toString());
|
event.completeTime = DateUtils::fromUtcString(query.value("complete_time").toString());
|
||||||
|
const int hasScreenshotIdx = query.record().indexOf("has_screenshot");
|
||||||
|
if (hasScreenshotIdx >= 0) {
|
||||||
|
event.hasScreenshot = query.value(hasScreenshotIdx).toBool();
|
||||||
|
}
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,7 +194,8 @@ QList<EventInfo> EventDAO::getEvents(int page, int pageSize) {
|
|||||||
QSqlQuery query(DatabaseManager::instance().database());
|
QSqlQuery query(DatabaseManager::instance().database());
|
||||||
query.prepare(R"(
|
query.prepare(R"(
|
||||||
SELECT id, account_id, device_id, external_id, amount, phone, bank_name,
|
SELECT id, account_id, device_id, external_id, amount, phone, bank_name,
|
||||||
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp
|
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp,
|
||||||
|
(LENGTH(screenshot) > 0) as has_screenshot
|
||||||
FROM events
|
FROM events
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT :limit OFFSET :offset
|
LIMIT :limit OFFSET :offset
|
||||||
@ -340,3 +346,24 @@ QByteArray EventDAO::getScreenshot(int id) {
|
|||||||
if (!query.exec() || !query.next()) return {};
|
if (!query.exec() || !query.next()) return {};
|
||||||
return query.value(0).toByteArray();
|
return query.value(0).toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<EventInfo> EventDAO::getRecentEvents(int hours) {
|
||||||
|
QList<EventInfo> events;
|
||||||
|
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 timestamp >= datetime('now', :hours)
|
||||||
|
ORDER BY id DESC
|
||||||
|
)");
|
||||||
|
query.bindValue(":hours", QString("-%1 hours").arg(hours));
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning() << "Ошибка при getRecentEvents:" << query.lastError().text();
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
while (query.next()) {
|
||||||
|
events.append(parseEvent(query));
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|||||||
@ -38,4 +38,6 @@ public:
|
|||||||
[[nodiscard]] static bool hasPendingEvents();
|
[[nodiscard]] static bool hasPendingEvents();
|
||||||
|
|
||||||
[[nodiscard]] static QSet<QString> getDevicesWithPendingEvents();
|
[[nodiscard]] static QSet<QString> getDevicesWithPendingEvents();
|
||||||
|
|
||||||
|
[[nodiscard]] static QList<EventInfo> getRecentEvents(int hours);
|
||||||
};
|
};
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@ -71,6 +71,7 @@ struct EventInfo {
|
|||||||
bool sentToServer = false;
|
bool sentToServer = false;
|
||||||
|
|
||||||
QByteArray screenshot;
|
QByteArray screenshot;
|
||||||
|
bool hasScreenshot = false;
|
||||||
|
|
||||||
QDateTime updateTime;
|
QDateTime updateTime;
|
||||||
QDateTime completeTime;
|
QDateTime completeTime;
|
||||||
|
|||||||
@ -29,6 +29,8 @@
|
|||||||
#include "ozon/PayByPhoneScript.h"
|
#include "ozon/PayByPhoneScript.h"
|
||||||
#include "dushanbe/LoginAndCheckAccountsScript.h"
|
#include "dushanbe/LoginAndCheckAccountsScript.h"
|
||||||
#include "dushanbe/GetLastTransactionsScript.h"
|
#include "dushanbe/GetLastTransactionsScript.h"
|
||||||
|
#include "dushanbe/PayByCardScript.h"
|
||||||
|
#include "dushanbe/PayByPhoneScript.h"
|
||||||
|
|
||||||
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data);
|
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data);
|
||||||
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description);
|
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description);
|
||||||
@ -125,6 +127,9 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
|
|||||||
event.json = QJsonDocument(task).toJson(QJsonDocument::Compact);
|
event.json = QJsonDocument(task).toJson(QJsonDocument::Compact);
|
||||||
|
|
||||||
const QJsonObject body = task["body"].toObject();
|
const QJsonObject body = task["body"].toObject();
|
||||||
|
if (eventType == EventType::FetchTransactions) {
|
||||||
|
event.amount = body["amount"].toDouble() / 100.0;
|
||||||
|
}
|
||||||
if (eventType == EventType::CreateTransaction) {
|
if (eventType == EventType::CreateTransaction) {
|
||||||
event.amount = body["amount"].toDouble() / 100.0;
|
event.amount = body["amount"].toDouble() / 100.0;
|
||||||
event.phone = body["bank_participator_id"].toString();
|
event.phone = body["bank_participator_id"].toString();
|
||||||
@ -169,12 +174,18 @@ static void sendTaskResult(const QString &type, const QString &materialId, const
|
|||||||
auto *networkService = new NetworkService;
|
auto *networkService = new NetworkService;
|
||||||
networkService->moveToThread(thread);
|
networkService->moveToThread(thread);
|
||||||
networkService->setPayload(QJsonDocument(obj).toJson());
|
networkService->setPayload(QJsonDocument(obj).toJson());
|
||||||
QObject::connect(thread, &QThread::started, networkService, [networkService, eventId]() {
|
QObject::connect(thread, &QThread::started, networkService, &NetworkService::postTaskResult);
|
||||||
networkService->postTaskResult();
|
QObject::connect(networkService, &NetworkService::finishedWithResult, networkService,
|
||||||
if (eventId != -1) {
|
[eventId](int result) {
|
||||||
EventDAO::markSentToServer(eventId);
|
if (eventId != -1) {
|
||||||
}
|
if (result == 0) {
|
||||||
});
|
EventDAO::markSentToServer(eventId);
|
||||||
|
} else {
|
||||||
|
EventDAO::updateEvent(eventId, EventStatus::Error, "Failed to send result to server");
|
||||||
|
qWarning() << "[EventHandler] Task result not sent, marking event" << eventId << "as error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
QObject::connect(networkService, &NetworkService::finished, thread, &QThread::quit);
|
QObject::connect(networkService, &NetworkService::finished, thread, &QThread::quit);
|
||||||
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
|
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
|
||||||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
@ -306,18 +317,26 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
QJsonObject extraObj;
|
QJsonObject extraObj;
|
||||||
extraObj["fee"] = static_cast<int>(tx.fee * 100);
|
extraObj["fee"] = static_cast<int>(tx.fee * 100);
|
||||||
txObj["extra"] = extraObj;
|
txObj["extra"] = extraObj;
|
||||||
if (tx.timestamp.isValid()) {
|
const QTimeZone bankTz(event.bankName == "dushanbe_city_bank"
|
||||||
txObj["created_at"] = tx.timestamp.toTimeZone(QTimeZone("Europe/Moscow")).toString(Qt::ISODateWithMs);
|
? "Asia/Dushanbe" : "Europe/Moscow");
|
||||||
} else if (!tx.bankTime.isEmpty()) {
|
// Приоритет: bankTime (время операции) > timestamp (время сохранения)
|
||||||
|
// Сервер ожидает UTC ("Z")
|
||||||
|
bool createdAtSet = false;
|
||||||
|
if (!tx.bankTime.isEmpty()) {
|
||||||
QDateTime dt = QDateTime::fromString(tx.bankTime, "dd.MM.yyyy, HH:mm");
|
QDateTime dt = QDateTime::fromString(tx.bankTime, "dd.MM.yyyy, HH:mm");
|
||||||
if (!dt.isValid()) dt = QDateTime::fromString(tx.bankTime, "dd.MM.yyyy HH:mm");
|
if (!dt.isValid()) dt = QDateTime::fromString(tx.bankTime, "dd.MM.yyyy HH:mm");
|
||||||
|
if (!dt.isValid()) dt = QDateTime::fromString(tx.bankTime, "dd.MM.yyyy HH:mm:ss");
|
||||||
if (dt.isValid()) {
|
if (dt.isValid()) {
|
||||||
dt.setTimeZone(QTimeZone("Europe/Moscow"));
|
dt.setTimeZone(bankTz);
|
||||||
txObj["created_at"] = dt.toString(Qt::ISODateWithMs);
|
txObj["created_at"] = dt.toUTC().toString(Qt::ISODate);
|
||||||
} else {
|
createdAtSet = true;
|
||||||
txObj["created_at"] = QJsonValue(QJsonValue::Null);
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
if (!createdAtSet && tx.timestamp.isValid()) {
|
||||||
|
txObj["created_at"] = tx.timestamp.toUTC().toString(Qt::ISODate);
|
||||||
|
createdAtSet = true;
|
||||||
|
}
|
||||||
|
if (!createdAtSet) {
|
||||||
txObj["created_at"] = QJsonValue(QJsonValue::Null);
|
txObj["created_at"] = QJsonValue(QJsonValue::Null);
|
||||||
}
|
}
|
||||||
arr.append(txObj);
|
arr.append(txObj);
|
||||||
@ -334,7 +353,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
obj["transaction_type"] = txType.isEmpty() ? "top-up" : txType;
|
obj["transaction_type"] = txType.isEmpty() ? "top-up" : txType;
|
||||||
obj["bank_participator_id"]= event.phone;
|
obj["bank_participator_id"]= event.phone;
|
||||||
obj["status"] = "completed";
|
obj["status"] = "completed";
|
||||||
obj["created_at"] = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow")).toString(Qt::ISODateWithMs);
|
obj["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||||
|
|
||||||
// Берём bank_transaction_id и fee из последней сохранённой транзакции
|
// Берём bank_transaction_id и fee из последней сохранённой транзакции
|
||||||
QList<TransactionInfo> txList = TransactionDAO::getTransactionsWithinHours(event.accountId, 1);
|
QList<TransactionInfo> txList = TransactionDAO::getTransactionsWithinHours(event.accountId, 1);
|
||||||
@ -420,7 +439,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
|||||||
if (event.type == EventType::FetchProfile) {
|
if (event.type == EventType::FetchProfile) {
|
||||||
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true));
|
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true));
|
||||||
else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(adbSerial, appCode));
|
else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(adbSerial, appCode));
|
||||||
else if (appCode == "dushanbe") setup(new Dushanbe::LoginAndCheckAccountsScript(adbSerial, appCode));
|
else if (appCode == "dushanbe_city_bank") setup(new Dushanbe::LoginAndCheckAccountsScript(adbSerial, appCode));
|
||||||
else qWarning() << "[EventHandler] FETCH_PROFILE: unknown appCode:" << appCode;
|
else qWarning() << "[EventHandler] FETCH_PROFILE: unknown appCode:" << appCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,7 +454,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
|||||||
setup(new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime));
|
setup(new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime));
|
||||||
else if (appCode == "black")
|
else if (appCode == "black")
|
||||||
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
|
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
|
||||||
else if (appCode == "dushanbe")
|
else if (appCode == "dushanbe_city_bank")
|
||||||
setup(new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, searchAmount, searchTime));
|
setup(new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, searchAmount, searchTime));
|
||||||
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
|
||||||
}
|
}
|
||||||
@ -451,10 +470,12 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
|||||||
// Перевод по номеру карты
|
// Перевод по номеру карты
|
||||||
if (appCode == "ozon") setup(new Ozon::PayByCardScript(account, event.phone, event.amount));
|
if (appCode == "ozon") setup(new Ozon::PayByCardScript(account, event.phone, event.amount));
|
||||||
else if (appCode == "black") setup(new Black::PayByCardScript(account, event.phone, event.amount));
|
else if (appCode == "black") setup(new Black::PayByCardScript(account, event.phone, event.amount));
|
||||||
|
else if (appCode == "dushanbe_city_bank") setup(new Dushanbe::PayByCardScript(account, event.phone, event.amount));
|
||||||
else qWarning() << "[EventHandler] CREATE_TRANSACTION bank: unknown appCode:" << appCode;
|
else qWarning() << "[EventHandler] CREATE_TRANSACTION bank: unknown appCode:" << appCode;
|
||||||
} else {
|
} else {
|
||||||
// Перевод по номеру телефона (top-up)
|
// Перевод по номеру телефона (top-up)
|
||||||
if (appCode == "ozon") setup(new Ozon::PayByPhoneScript(account, event.phone, recipientBank, event.amount));
|
if (appCode == "ozon") setup(new Ozon::PayByPhoneScript(account, event.phone, recipientBank, event.amount));
|
||||||
|
else if (appCode == "dushanbe_city_bank") setup(new Dushanbe::PayByPhoneScript(account, event.phone, event.amount));
|
||||||
else qWarning() << "[EventHandler] CREATE_TRANSACTION: unknown appCode:" << appCode;
|
else qWarning() << "[EventHandler] CREATE_TRANSACTION: unknown appCode:" << appCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1228,6 +1228,9 @@ void NetworkService::postTaskResult() {
|
|||||||
const QJsonValue result = postJson(m_apiBase + m_pathTaskResult, body, true);
|
const QJsonValue result = postJson(m_apiBase + m_pathTaskResult, body, true);
|
||||||
if (result.isUndefined()) {
|
if (result.isUndefined()) {
|
||||||
qWarning() << "[NetworkService] postTaskResult failed, body:" << QJsonDocument(body).toJson(QJsonDocument::Compact);
|
qWarning() << "[NetworkService] postTaskResult failed, body:" << QJsonDocument(body).toJson(QJsonDocument::Compact);
|
||||||
|
emit finishedWithResult(-1);
|
||||||
|
} else {
|
||||||
|
emit finishedWithResult(0);
|
||||||
}
|
}
|
||||||
emit finished();
|
emit finished();
|
||||||
}
|
}
|
||||||
@ -1251,8 +1254,13 @@ void NetworkService::postMonitoringLog(const QString &type, const QString &event
|
|||||||
multiPart->append(imagePart);
|
multiPart->append(imagePart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureValidToken();
|
||||||
|
|
||||||
const QUrl url(m_apiBase + m_pathMonitoringLog);
|
const QUrl url(m_apiBase + m_pathMonitoringLog);
|
||||||
QNetworkRequest request(url);
|
QNetworkRequest request(url);
|
||||||
|
if (!m_accessToken.isEmpty()) {
|
||||||
|
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||||
multiPart->setParent(reply);
|
multiPart->setParent(reply);
|
||||||
|
|||||||
BIN
tutorials/image_1.jpg
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
tutorials/image_2.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
@ -1,17 +0,0 @@
|
|||||||
# Руководство: Запуск приложения TutorialWindow
|
|
||||||
|
|
||||||
Этот файл объясняет, как собрать и запустить ваше Qt6-приложение с окном TutorialWindow.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Требования
|
|
||||||
|
|
||||||
- Qt 6 (рекомендуется версия 6.2 или выше)
|
|
||||||
- C++17-совместимый компилятор (GCC, Clang, MSVC)
|
|
||||||
- CMake (версия ≥ 3.16) или qmake
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Структура проекта
|
|
||||||
[Текст ссылки](https://example.com)
|
|
||||||
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# Руководство: Запуск приложения TutorialWindow
|
|
||||||
|
|
||||||
Этот файл объясняет, как собрать и запустить ваше Qt6-приложение с окном TutorialWindow.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Требования
|
|
||||||
|
|
||||||
- Qt 6 (рекомендуется версия 6.2 или выше)
|
|
||||||
- C++17-совместимый компилятор (GCC, Clang, MSVC)
|
|
||||||
- CMake (версия ≥ 3.16) или qmake
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Структура проекта
|
|
||||||
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# Руководство: Запуск приложения TutorialWindow
|
|
||||||
|
|
||||||
Этот файл объясняет, как собрать и запустить ваше Qt6-приложение с окном TutorialWindow.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Требования
|
|
||||||
|
|
||||||
- Qt 6 (рекомендуется версия 6.2 или выше)
|
|
||||||
- C++17-совместимый компилятор (GCC, Clang, MSVC)
|
|
||||||
- CMake (версия ≥ 3.16) или qmake
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Структура проекта
|
|
||||||
|
|
||||||
38
tutorials/ui_info.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Описание интерфейса
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Главный экран — Устройство
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="40%"><img src="image_1.jpg" width="280"/></td>
|
||||||
|
<td valign="top">
|
||||||
|
<p><b>1</b> — Название устройства и ID</p>
|
||||||
|
<p><b>2</b> — Кнопка настроек устройств (там вы увидите какие банки поддерживаются приложением)</p>
|
||||||
|
<p><b>3</b> — Кнопка удаления устройства (удалит все данные устройства и если подключите снова, создаст новое)</p>
|
||||||
|
<p><b>4</b> — Статус банковского профиля, красный - выключен, зеленый - включен</p>
|
||||||
|
<p><b>5</b> — Настройки банковского профиля. Если нет то самый первый раз будет мастер настроеек профиля. </p>
|
||||||
|
<p><b>6</b> — Имя профиля и номер телефона банковского профиля (если настроен)</p>
|
||||||
|
<p><b>7</b> — Кнопка включения/выключения банковского профиля</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Настройки банка
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="55%"><img src="image_2.jpg" width="450"/></td>
|
||||||
|
<td valign="top">
|
||||||
|
<p><b>1</b> — Включить / выключить профиль банка</p>
|
||||||
|
<p><b>2</b> — Редактировать пин-код</p>
|
||||||
|
<p><b>3</b> — Проверить пин-код на устройстве</p>
|
||||||
|
<p><b>4</b> — Включить / выключить счёт</p>
|
||||||
|
<p><b>5</b> — Обновить профиль (ФИО, телефон, баланс)</p>
|
||||||
|
<p><b>6</b> — Проверить аккаунты (карты и счета)</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
@ -8,11 +8,16 @@
|
|||||||
#include <QMenuBar>
|
#include <QMenuBar>
|
||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
|
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
#include "TutorialWindow.h"
|
#include "TutorialWindow.h"
|
||||||
#include "bank/BankListWindow.h"
|
#include "bank/BankListWindow.h"
|
||||||
#include "event/EventWindow.h"
|
#include "event/EventWindow.h"
|
||||||
#include "log/LogWindow.h"
|
#include "log/LogWindow.h"
|
||||||
#include "log/FileLogWindow.h"
|
#include "log/FileLogWindow.h"
|
||||||
|
#include "dao/GeneralLogDAO.h"
|
||||||
|
#include "net/NetworkService.h"
|
||||||
#include "db/DeviceInfo.h"
|
#include "db/DeviceInfo.h"
|
||||||
#include "device/DeviceWidget.h"
|
#include "device/DeviceWidget.h"
|
||||||
#include "dao/DeviceDAO.h"
|
#include "dao/DeviceDAO.h"
|
||||||
@ -96,13 +101,19 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
|||||||
m_eventWindow->activateWindow();
|
m_eventWindow->activateWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(fileLogAction, &QAction::triggered, this, [=]() {
|
connect(fileLogAction, &QAction::triggered, this, [this]() {
|
||||||
if (!m_fileLogWindow) {
|
QMessageBox msgBox(this);
|
||||||
m_fileLogWindow = new FileLogWindow(this);
|
msgBox.setWindowTitle("Логи");
|
||||||
|
msgBox.setText("Отправить логи на сервер?");
|
||||||
|
auto *yesBtn = msgBox.addButton("Отправить", QMessageBox::YesRole);
|
||||||
|
msgBox.addButton("Отмена", QMessageBox::NoRole);
|
||||||
|
msgBox.exec();
|
||||||
|
if (msgBox.clickedButton() == yesBtn) {
|
||||||
|
if (!m_fileLogWindow) {
|
||||||
|
m_fileLogWindow = new FileLogWindow(this);
|
||||||
|
}
|
||||||
|
m_fileLogWindow->sendLogToTelegram();
|
||||||
}
|
}
|
||||||
m_fileLogWindow->show();
|
|
||||||
m_fileLogWindow->raise();
|
|
||||||
m_fileLogWindow->activateWindow();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (SettingsDAO::get("tutorial_shown").isEmpty()) {
|
if (SettingsDAO::get("tutorial_shown").isEmpty()) {
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
#include "TutorialWindow.h"
|
#include "TutorialWindow.h"
|
||||||
#include "bank/AccountWindow.h"
|
#include "bank/AccountWindow.h"
|
||||||
#include "db/DeviceInfo.h"
|
#include "db/DeviceInfo.h"
|
||||||
|
#include "log/FileLogWindow.h"
|
||||||
#include "widget/common/FlowLayout.h"
|
#include "widget/common/FlowLayout.h"
|
||||||
|
|
||||||
class MainWindow final : public QMainWindow {
|
class MainWindow final : public QMainWindow {
|
||||||
@ -32,7 +33,7 @@ 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_fileLogWindow = nullptr;
|
FileLogWindow *m_fileLogWindow = nullptr;
|
||||||
QWidget *m_eventWindow = nullptr;
|
QWidget *m_eventWindow = nullptr;
|
||||||
|
|
||||||
void createDevicePage();
|
void createDevicePage();
|
||||||
|
|||||||
@ -76,7 +76,7 @@ void BankSettingsWindow::startCheckPinCode(const QString &appCode) {
|
|||||||
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);
|
||||||
} else if (appCode == "dushanbe") {
|
} else if (appCode == "dushanbe_city_bank") {
|
||||||
auto *worker = new Dushanbe::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr);
|
auto *worker = new Dushanbe::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr);
|
||||||
worker->moveToThread(thread);
|
worker->moveToThread(thread);
|
||||||
connect(thread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start);
|
connect(thread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start);
|
||||||
|
|||||||
@ -398,7 +398,7 @@ void BankSetupWizard::runPinCheck() {
|
|||||||
}, Qt::QueuedConnection);
|
}, Qt::QueuedConnection);
|
||||||
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||||||
connect(worker, &Black::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit);
|
connect(worker, &Black::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit);
|
||||||
} else if (appCode == "dushanbe") {
|
} else if (appCode == "dushanbe_city_bank") {
|
||||||
auto *worker = new Dushanbe::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true);
|
auto *worker = new Dushanbe::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true);
|
||||||
worker->moveToThread(m_workerThread);
|
worker->moveToThread(m_workerThread);
|
||||||
connect(m_workerThread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start);
|
connect(m_workerThread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start);
|
||||||
@ -459,7 +459,7 @@ void BankSetupWizard::runProfileCheck() {
|
|||||||
connect(worker, &Black::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection);
|
connect(worker, &Black::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection);
|
||||||
connect(worker, &Black::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
connect(worker, &Black::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
connect(worker, &Black::GetProfileInfoScript::finished, m_workerThread, &QThread::quit);
|
connect(worker, &Black::GetProfileInfoScript::finished, m_workerThread, &QThread::quit);
|
||||||
} else if (appCode == "dushanbe") {
|
} else if (appCode == "dushanbe_city_bank") {
|
||||||
auto *worker = new Dushanbe::GetProfileInfoScript(deviceId, appCode, nullptr);
|
auto *worker = new Dushanbe::GetProfileInfoScript(deviceId, appCode, nullptr);
|
||||||
worker->moveToThread(m_workerThread);
|
worker->moveToThread(m_workerThread);
|
||||||
connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetProfileInfoScript::start);
|
connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetProfileInfoScript::start);
|
||||||
@ -514,7 +514,7 @@ void BankSetupWizard::runCardCheck() {
|
|||||||
connect(worker, &Black::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection);
|
connect(worker, &Black::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection);
|
||||||
connect(worker, &Black::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
connect(worker, &Black::GetCardInfoScript::finished, worker, &QObject::deleteLater);
|
||||||
connect(worker, &Black::GetCardInfoScript::finished, m_workerThread, &QThread::quit);
|
connect(worker, &Black::GetCardInfoScript::finished, m_workerThread, &QThread::quit);
|
||||||
} else if (appCode == "dushanbe") {
|
} else if (appCode == "dushanbe_city_bank") {
|
||||||
auto *worker = new Dushanbe::GetCardInfoScript(deviceId, appCode, nullptr);
|
auto *worker = new Dushanbe::GetCardInfoScript(deviceId, appCode, nullptr);
|
||||||
worker->moveToThread(m_workerThread);
|
worker->moveToThread(m_workerThread);
|
||||||
connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetCardInfoScript::start);
|
connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetCardInfoScript::start);
|
||||||
|
|||||||
@ -147,7 +147,7 @@ void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) {
|
|||||||
|
|
||||||
connect(settingsBtn, &QPushButton::clicked, this, [this, app, tableWidget]() {
|
connect(settingsBtn, &QPushButton::clicked, this, [this, app, tableWidget]() {
|
||||||
const BankProfileInfo dbApp = BankProfileDAO::getApplication(m_deviceId, app.code);
|
const BankProfileInfo dbApp = BankProfileDAO::getApplication(m_deviceId, app.code);
|
||||||
if (dbApp.id < 0 || dbApp.bankProfileId.isEmpty()) {
|
if (dbApp.id < 0 || !dbApp.pinCodeChecked) {
|
||||||
auto *wizard = new BankSetupWizard(this, m_deviceId, dbApp.id >= 0 ? dbApp : app);
|
auto *wizard = new BankSetupWizard(this, m_deviceId, dbApp.id >= 0 ? dbApp : app);
|
||||||
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
connect(wizard, &QDialog::finished, this, [this, tableWidget]() {
|
connect(wizard, &QDialog::finished, this, [this, tableWidget]() {
|
||||||
|
|||||||
@ -239,7 +239,7 @@ void DeviceWidget::setOpenBanksSettings(QPushButton *button, const BankProfileIn
|
|||||||
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]() {
|
||||||
const BankProfileInfo dbApp = BankProfileDAO::getApplication(deviceId, app.code);
|
const BankProfileInfo dbApp = BankProfileDAO::getApplication(deviceId, app.code);
|
||||||
if (dbApp.id < 0 || dbApp.bankProfileId.isEmpty()) {
|
if (dbApp.id < 0 || !dbApp.pinCodeChecked) {
|
||||||
auto *wizard = new BankSetupWizard(nullptr, deviceId, dbApp.id >= 0 ? dbApp : app);
|
auto *wizard = new BankSetupWizard(nullptr, deviceId, dbApp.id >= 0 ? dbApp : app);
|
||||||
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
wizard->show();
|
wizard->show();
|
||||||
|
|||||||
@ -1,15 +1,23 @@
|
|||||||
#include "EventWindow.h"
|
#include "EventWindow.h"
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
|
#include <QHttpMultiPart>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
#include <QPlainTextEdit>
|
#include <QPlainTextEdit>
|
||||||
|
#include <QProcess>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "dao/EventDAO.h"
|
#include "dao/EventDAO.h"
|
||||||
|
#include "dao/SettingsDAO.h"
|
||||||
#include "net/NetworkService.h"
|
#include "net/NetworkService.h"
|
||||||
|
|
||||||
static constexpr int COL_ID = 0;
|
static constexpr int COL_ID = 0;
|
||||||
@ -22,7 +30,8 @@ static constexpr int COL_AMOUNT = 6;
|
|||||||
static constexpr int COL_COMMENT = 7;
|
static constexpr int COL_COMMENT = 7;
|
||||||
static constexpr int COL_JSON = 8;
|
static constexpr int COL_JSON = 8;
|
||||||
static constexpr int COL_SENT = 9;
|
static constexpr int COL_SENT = 9;
|
||||||
static constexpr int COL_ACTION = 10;
|
static constexpr int COL_IMG = 10;
|
||||||
|
static constexpr int COL_ACTION = 11;
|
||||||
|
|
||||||
static void sendErrorToServer(const QString &type, const QString &materialId, const QString &bankName) {
|
static void sendErrorToServer(const QString &type, const QString &materialId, const QString &bankName) {
|
||||||
QJsonObject errData;
|
QJsonObject errData;
|
||||||
@ -71,15 +80,15 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
|
|||||||
|
|
||||||
// ── Таблица ───────────────────────────────────────────────────────────────
|
// ── Таблица ───────────────────────────────────────────────────────────────
|
||||||
m_table = new QTableWidget(this);
|
m_table = new QTableWidget(this);
|
||||||
m_table->setColumnCount(11);
|
m_table->setColumnCount(12);
|
||||||
m_table->setHorizontalHeaderLabels({
|
m_table->setHorizontalHeaderLabels({
|
||||||
"#", "Время", "Тип", "Банк", "Устройство", "Статус", "Сумма", "Комментарий", "JSON", "Отпр.", ""
|
"#", "Время", "Тип", "Банк", "Устройство", "Статус", "Сумма", "Комментарий", "JSON", "Отпр.", "Скрин", ""
|
||||||
});
|
});
|
||||||
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||||
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
|
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
m_table->verticalHeader()->hide();
|
m_table->verticalHeader()->hide();
|
||||||
m_table->horizontalHeader()->setStretchLastSection(true);
|
m_table->horizontalHeader()->setStretchLastSection(false);
|
||||||
m_table->setColumnWidth(COL_ID, 45);
|
m_table->setColumnWidth(COL_ID, 45);
|
||||||
m_table->setColumnWidth(COL_TIME, 145);
|
m_table->setColumnWidth(COL_TIME, 145);
|
||||||
m_table->setColumnWidth(COL_TYPE, 100);
|
m_table->setColumnWidth(COL_TYPE, 100);
|
||||||
@ -87,9 +96,11 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
|
|||||||
m_table->setColumnWidth(COL_DEVICE, 120);
|
m_table->setColumnWidth(COL_DEVICE, 120);
|
||||||
m_table->setColumnWidth(COL_STATUS, 100);
|
m_table->setColumnWidth(COL_STATUS, 100);
|
||||||
m_table->setColumnWidth(COL_AMOUNT, 80);
|
m_table->setColumnWidth(COL_AMOUNT, 80);
|
||||||
m_table->setColumnWidth(COL_JSON, 200);
|
m_table->setColumnHidden(COL_JSON, true);
|
||||||
|
m_table->setColumnHidden(COL_ACTION, true);
|
||||||
m_table->setColumnWidth(COL_SENT, 45);
|
m_table->setColumnWidth(COL_SENT, 45);
|
||||||
m_table->setColumnWidth(COL_ACTION, 80);
|
m_table->setColumnWidth(COL_IMG, 45);
|
||||||
|
m_table->horizontalHeader()->setSectionResizeMode(COL_COMMENT, QHeaderView::Stretch);
|
||||||
|
|
||||||
// ── Панель скриншота ──────────────────────────────────────────────────────
|
// ── Панель скриншота ──────────────────────────────────────────────────────
|
||||||
m_screenshotLabel = new QLabel(this);
|
m_screenshotLabel = new QLabel(this);
|
||||||
@ -112,6 +123,8 @@ EventWindow::EventWindow(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);
|
||||||
@ -119,6 +132,7 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
|
|||||||
paginationLayout->addWidget(m_nextBtn);
|
paginationLayout->addWidget(m_nextBtn);
|
||||||
paginationLayout->addStretch();
|
paginationLayout->addStretch();
|
||||||
paginationLayout->addWidget(refreshBtn);
|
paginationLayout->addWidget(refreshBtn);
|
||||||
|
paginationLayout->addWidget(m_sendBtn);
|
||||||
|
|
||||||
auto *mainLayout = new QVBoxLayout(this);
|
auto *mainLayout = new QVBoxLayout(this);
|
||||||
mainLayout->addWidget(splitter, 1);
|
mainLayout->addWidget(splitter, 1);
|
||||||
@ -140,6 +154,8 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
|
|||||||
showScreenshot(m_eventIds[row]);
|
showScreenshot(m_eventIds[row]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
connect(m_sendBtn, &QPushButton::clicked, this, &EventWindow::sendEventsToCsv);
|
||||||
|
|
||||||
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);
|
||||||
@ -190,6 +206,7 @@ void EventWindow::loadPage() {
|
|||||||
m_table->setItem(i, COL_JSON, jsonItem);
|
m_table->setItem(i, COL_JSON, jsonItem);
|
||||||
|
|
||||||
m_table->setItem(i, COL_SENT, new QTableWidgetItem(e.sentToServer ? "+" : ""));
|
m_table->setItem(i, COL_SENT, new QTableWidgetItem(e.sentToServer ? "+" : ""));
|
||||||
|
m_table->setItem(i, COL_IMG, new QTableWidgetItem(e.hasScreenshot ? "+" : ""));
|
||||||
|
|
||||||
// Подсветка строки по статусу
|
// Подсветка строки по статусу
|
||||||
QColor bg;
|
QColor bg;
|
||||||
@ -247,3 +264,138 @@ void EventWindow::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 EventWindow::sendEventsToCsv() {
|
||||||
|
const QList<EventInfo> events = EventDAO::getRecentEvents(48);
|
||||||
|
if (events.isEmpty()) {
|
||||||
|
QMessageBox::information(this, "События", "Нет событий за последние 48 часов.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_sendBtn->setEnabled(false);
|
||||||
|
m_sendBtn->setText("Отправка...");
|
||||||
|
|
||||||
|
// Формируем CSV
|
||||||
|
QString csv;
|
||||||
|
csv += "id;time;type;bank;device;status;amount;phone;comment;sent\n";
|
||||||
|
for (const auto &e : events) {
|
||||||
|
csv += QString("%1;%2;%3;%4;%5;%6;%7;%8;%9;%10\n")
|
||||||
|
.arg(e.id)
|
||||||
|
.arg(e.timestamp.toLocalTime().toString("dd.MM.yyyy HH:mm:ss"))
|
||||||
|
.arg(eventTypeToString(e.type))
|
||||||
|
.arg(e.bankName)
|
||||||
|
.arg(e.deviceId)
|
||||||
|
.arg(eventStatusToString(e.status))
|
||||||
|
.arg(e.amount, 0, 'f', 2)
|
||||||
|
.arg(e.phone)
|
||||||
|
.arg(QString(e.comment).replace(';', ','))
|
||||||
|
.arg(e.sentToServer ? "yes" : "no");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Собираем временную папку
|
||||||
|
const QString tmpDir = QDir::tempPath() + "/arc_events_export";
|
||||||
|
QDir().mkpath(tmpDir);
|
||||||
|
QDir().mkpath(tmpDir + "/screenshots");
|
||||||
|
|
||||||
|
// Сохраняем CSV
|
||||||
|
{
|
||||||
|
QFile csvFile(tmpDir + "/events.csv");
|
||||||
|
if (csvFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
csvFile.write(csv.toUtf8());
|
||||||
|
csvFile.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем скриншоты
|
||||||
|
int screenshotCount = 0;
|
||||||
|
for (const auto &e : events) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём ZIP
|
||||||
|
const QString zipPath = QDir::tempPath() + "/arc_events.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем в Telegram
|
||||||
|
const QString botToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8";
|
||||||
|
const QString chatId = "-5177086220";
|
||||||
|
const QString desktopId = SettingsDAO::get("desktop_id");
|
||||||
|
const QString caption = "Events (48h) from desktop: " + desktopId
|
||||||
|
+ "\nTotal: " + QString::number(events.size())
|
||||||
|
+ "\nScreenshots: " + QString::number(screenshotCount);
|
||||||
|
|
||||||
|
auto *sendFile = new QFile(zipPath);
|
||||||
|
if (!sendFile->open(QIODevice::ReadOnly)) {
|
||||||
|
QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив");
|
||||||
|
delete sendFile;
|
||||||
|
m_sendBtn->setEnabled(true);
|
||||||
|
m_sendBtn->setText("Отправить");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_events.zip")");
|
||||||
|
filePart.setBodyDevice(sendFile);
|
||||||
|
sendFile->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, "Готово", "События отправлены.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ private:
|
|||||||
QLabel *m_screenshotLabel;
|
QLabel *m_screenshotLabel;
|
||||||
QPushButton *m_prevBtn;
|
QPushButton *m_prevBtn;
|
||||||
QPushButton *m_nextBtn;
|
QPushButton *m_nextBtn;
|
||||||
|
QPushButton *m_sendBtn;
|
||||||
|
|
||||||
int m_page = 0;
|
int m_page = 0;
|
||||||
int m_pageSize = 50;
|
int m_pageSize = 50;
|
||||||
@ -28,4 +29,5 @@ private:
|
|||||||
void loadPage();
|
void loadPage();
|
||||||
void updatePagination();
|
void updatePagination();
|
||||||
void showScreenshot(int eventId);
|
void showScreenshot(int eventId);
|
||||||
|
void sendEventsToCsv();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class FileLogWindow final : public QDialog {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FileLogWindow(QWidget *parent = nullptr);
|
explicit FileLogWindow(QWidget *parent = nullptr);
|
||||||
|
void sendLogToTelegram();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTableWidget *m_table;
|
QTableWidget *m_table;
|
||||||
@ -33,5 +34,4 @@ private:
|
|||||||
void loadPage();
|
void loadPage();
|
||||||
void updatePagination();
|
void updatePagination();
|
||||||
void showScreenshot(int row);
|
void showScreenshot(int row);
|
||||||
void sendLogToTelegram();
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QTextBrowser>
|
#include <QTextBrowser>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
|
||||||
CollapsibleSection::CollapsibleSection(
|
CollapsibleSection::CollapsibleSection(
|
||||||
@ -26,7 +28,20 @@ CollapsibleSection::CollapsibleSection(
|
|||||||
|
|
||||||
// Контент без собственного скрола, займёт доступное пространство
|
// Контент без собственного скрола, займёт доступное пространство
|
||||||
m_contentBrowser = new QTextBrowser(this);
|
m_contentBrowser = new QTextBrowser(this);
|
||||||
m_contentBrowser->setMarkdown(markdownContent);
|
m_contentBrowser->setSearchPaths({QCoreApplication::applicationDirPath() + "/tutorials"});
|
||||||
|
if (markdownContent.contains("<table") || markdownContent.contains("<img")) {
|
||||||
|
// Контент с HTML — конвертируем markdown-элементы вручную
|
||||||
|
QString html = markdownContent;
|
||||||
|
// ## заголовок → <h2>
|
||||||
|
html.replace(QRegularExpression(R"(^## (.+)$)", QRegularExpression::MultilineOption), R"(<h3>\1</h3>)");
|
||||||
|
// --- → <hr>
|
||||||
|
html.replace(QRegularExpression(R"(^---$)", QRegularExpression::MultilineOption), "<hr>");
|
||||||
|
// **text** → <b>text</b>
|
||||||
|
html.replace(QRegularExpression(R"(\*\*([^*]+)\*\*)"), R"(<b>\1</b>)");
|
||||||
|
m_contentBrowser->setHtml(html);
|
||||||
|
} else {
|
||||||
|
m_contentBrowser->setMarkdown(markdownContent);
|
||||||
|
}
|
||||||
m_contentBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
m_contentBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
m_contentBrowser->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
m_contentBrowser->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
m_contentBrowser->setFrameShape(QFrame::NoFrame);
|
m_contentBrowser->setFrameShape(QFrame::NoFrame);
|
||||||
|
|||||||