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["card_number"] = savedAccount.cardNumber;
|
||||
materialBody["name"] = updatedApp.fullName;
|
||||
materialBody["state"] = materialStatusToString(savedAccount.status) == "active" ? "enabled" : "disabled";
|
||||
ns.setPayload(QJsonDocument(materialBody).toJson());
|
||||
ns.addExtra("account_id", savedAccount.id);
|
||||
ns.postMaterial();
|
||||
|
||||
@ -174,8 +174,9 @@ void GetCardInfoScript::doStart() {
|
||||
ns.setDeviceId(m_deviceId);
|
||||
QJsonObject materialBody;
|
||||
materialBody["bank_profile_id"] = updatedApp.bankProfileId;
|
||||
materialBody["card_number"] = fullNumber;
|
||||
materialBody["card_number"] = QString(fullNumber).remove(' ');
|
||||
materialBody["name"] = updatedApp.fullName;
|
||||
materialBody["state"] = materialStatusToString(savedAccount.status) == "active" ? "enabled" : "disabled";
|
||||
ns.setPayload(QJsonDocument(materialBody).toJson());
|
||||
ns.addExtra("account_id", savedAccount.id);
|
||||
ns.postMaterial();
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
#include "GetLastTransactionsScript.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QThread>
|
||||
#include <QTimeZone>
|
||||
|
||||
#include <tesseract/baseapi.h>
|
||||
#include <leptonica/allheaders.h>
|
||||
|
||||
#include "adb/AdbUtils.h"
|
||||
#include "dao/MaterialDAO.h"
|
||||
#include "dao/BankProfileDAO.h"
|
||||
@ -10,6 +14,215 @@
|
||||
#include "dao/TransactionDAO.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 {
|
||||
|
||||
GetLastTransactionsScript::GetLastTransactionsScript(
|
||||
@ -117,7 +330,7 @@ void GetLastTransactionsScript::doStart() {
|
||||
// Ищем соответствующий node чтобы по нему тапнуть
|
||||
Node txNode;
|
||||
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;
|
||||
// Проверяем что содержит нужную сумму
|
||||
QString amountStr = QString::number(tx.amount, 'f', 2);
|
||||
@ -133,13 +346,14 @@ void GetLastTransactionsScript::doStart() {
|
||||
}
|
||||
|
||||
AdbUtils::makeTap(m_deviceId, txNode.x(), txNode.y());
|
||||
QThread::msleep(2000);
|
||||
QThread::msleep(3000);
|
||||
|
||||
// Ждём экран деталей
|
||||
// Ждём экран деталей — ищем кнопку "Назад" (признак экрана детали)
|
||||
bool detailLoaded = false;
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
if (xmlScreenParser.isTransactionDetailScreen()) {
|
||||
if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Назад")).isEmpty() ||
|
||||
!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Поделиться")).isEmpty()) {
|
||||
detailLoaded = true;
|
||||
break;
|
||||
}
|
||||
@ -153,7 +367,18 @@ void GetLastTransactionsScript::doStart() {
|
||||
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 час
|
||||
if (m_searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) {
|
||||
@ -193,7 +418,7 @@ void GetLastTransactionsScript::doStart() {
|
||||
saveTx.description = detail.provider;
|
||||
saveTx.phone = detail.receiverAccount;
|
||||
saveTx.name = detail.receiverAccount;
|
||||
saveTx.status = (detail.status == QString::fromUtf8("Успешный"))
|
||||
saveTx.status = detail.status.contains(QString::fromUtf8("Успешн"))
|
||||
? TransactionStatus::Complete : TransactionStatus::Unknown;
|
||||
saveTx.type = TransactionType::Phone;
|
||||
saveTx.timestamp = QDateTime::currentDateTimeUtc();
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QThread>
|
||||
|
||||
#include "adb/AdbUtils.h"
|
||||
@ -96,7 +97,8 @@ void GetProfileInfoScript::doStart() {
|
||||
|
||||
// Парсим ФИО и телефон из бокового меню
|
||||
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;
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
#include "GetProfileInfoScript.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QRegularExpression>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#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]
|
||||
list=ozon, black, dushanbe
|
||||
list=ozon, black, dushanbe_city_bank
|
||||
|
||||
[common]
|
||||
currencies=USD, ARS, RUB, TJS
|
||||
@ -30,9 +30,8 @@ currency=RUB
|
||||
name=Ozon
|
||||
phone_code=7
|
||||
|
||||
[dushanbe]
|
||||
[dushanbe_city_bank]
|
||||
android_package_name=tj.dc.next1
|
||||
currency=TJS
|
||||
name=Dushanbe City
|
||||
phone_code=992
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include <QSqlError>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlRecord>
|
||||
|
||||
#include "DatabaseManager.h"
|
||||
#include "time/DateUtils.h"
|
||||
@ -23,6 +24,10 @@ EventInfo parseEvent(const QSqlQuery &query) {
|
||||
event.timestamp = DateUtils::fromUtcString(query.value("timestamp").toString());
|
||||
event.updateTime = DateUtils::fromUtcString(query.value("update_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;
|
||||
}
|
||||
|
||||
@ -189,7 +194,8 @@ QList<EventInfo> EventDAO::getEvents(int page, int pageSize) {
|
||||
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
|
||||
status, type, comment, json, sent_to_server, update_time, complete_time, timestamp,
|
||||
(LENGTH(screenshot) > 0) as has_screenshot
|
||||
FROM events
|
||||
ORDER BY id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
@ -340,3 +346,24 @@ QByteArray EventDAO::getScreenshot(int id) {
|
||||
if (!query.exec() || !query.next()) return {};
|
||||
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 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;
|
||||
|
||||
QByteArray screenshot;
|
||||
bool hasScreenshot = false;
|
||||
|
||||
QDateTime updateTime;
|
||||
QDateTime completeTime;
|
||||
|
||||
@ -29,6 +29,8 @@
|
||||
#include "ozon/PayByPhoneScript.h"
|
||||
#include "dushanbe/LoginAndCheckAccountsScript.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 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);
|
||||
|
||||
const QJsonObject body = task["body"].toObject();
|
||||
if (eventType == EventType::FetchTransactions) {
|
||||
event.amount = body["amount"].toDouble() / 100.0;
|
||||
}
|
||||
if (eventType == EventType::CreateTransaction) {
|
||||
event.amount = body["amount"].toDouble() / 100.0;
|
||||
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;
|
||||
networkService->moveToThread(thread);
|
||||
networkService->setPayload(QJsonDocument(obj).toJson());
|
||||
QObject::connect(thread, &QThread::started, networkService, [networkService, eventId]() {
|
||||
networkService->postTaskResult();
|
||||
if (eventId != -1) {
|
||||
EventDAO::markSentToServer(eventId);
|
||||
}
|
||||
});
|
||||
QObject::connect(thread, &QThread::started, networkService, &NetworkService::postTaskResult);
|
||||
QObject::connect(networkService, &NetworkService::finishedWithResult, networkService,
|
||||
[eventId](int result) {
|
||||
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, networkService, &QObject::deleteLater);
|
||||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||
@ -306,18 +317,26 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
||||
QJsonObject extraObj;
|
||||
extraObj["fee"] = static_cast<int>(tx.fee * 100);
|
||||
txObj["extra"] = extraObj;
|
||||
if (tx.timestamp.isValid()) {
|
||||
txObj["created_at"] = tx.timestamp.toTimeZone(QTimeZone("Europe/Moscow")).toString(Qt::ISODateWithMs);
|
||||
} else if (!tx.bankTime.isEmpty()) {
|
||||
const QTimeZone bankTz(event.bankName == "dushanbe_city_bank"
|
||||
? "Asia/Dushanbe" : "Europe/Moscow");
|
||||
// Приоритет: bankTime (время операции) > timestamp (время сохранения)
|
||||
// Сервер ожидает UTC ("Z")
|
||||
bool createdAtSet = false;
|
||||
if (!tx.bankTime.isEmpty()) {
|
||||
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:ss");
|
||||
if (dt.isValid()) {
|
||||
dt.setTimeZone(QTimeZone("Europe/Moscow"));
|
||||
txObj["created_at"] = dt.toString(Qt::ISODateWithMs);
|
||||
} else {
|
||||
txObj["created_at"] = QJsonValue(QJsonValue::Null);
|
||||
dt.setTimeZone(bankTz);
|
||||
txObj["created_at"] = dt.toUTC().toString(Qt::ISODate);
|
||||
createdAtSet = true;
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
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["bank_participator_id"]= event.phone;
|
||||
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 из последней сохранённой транзакции
|
||||
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 (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true));
|
||||
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;
|
||||
}
|
||||
|
||||
@ -435,7 +454,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
||||
setup(new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime));
|
||||
else if (appCode == "black")
|
||||
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
|
||||
else if (appCode == "dushanbe")
|
||||
else if (appCode == "dushanbe_city_bank")
|
||||
setup(new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, searchAmount, searchTime));
|
||||
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));
|
||||
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 {
|
||||
// Перевод по номеру телефона (top-up)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1228,6 +1228,9 @@ void NetworkService::postTaskResult() {
|
||||
const QJsonValue result = postJson(m_apiBase + m_pathTaskResult, body, true);
|
||||
if (result.isUndefined()) {
|
||||
qWarning() << "[NetworkService] postTaskResult failed, body:" << QJsonDocument(body).toJson(QJsonDocument::Compact);
|
||||
emit finishedWithResult(-1);
|
||||
} else {
|
||||
emit finishedWithResult(0);
|
||||
}
|
||||
emit finished();
|
||||
}
|
||||
@ -1251,8 +1254,13 @@ void NetworkService::postMonitoringLog(const QString &type, const QString &event
|
||||
multiPart->append(imagePart);
|
||||
}
|
||||
|
||||
ensureValidToken();
|
||||
|
||||
const QUrl url(m_apiBase + m_pathMonitoringLog);
|
||||
QNetworkRequest request(url);
|
||||
if (!m_accessToken.isEmpty()) {
|
||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||
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 <QStackedWidget>
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QThread>
|
||||
|
||||
#include "TutorialWindow.h"
|
||||
#include "bank/BankListWindow.h"
|
||||
#include "event/EventWindow.h"
|
||||
#include "log/LogWindow.h"
|
||||
#include "log/FileLogWindow.h"
|
||||
#include "dao/GeneralLogDAO.h"
|
||||
#include "net/NetworkService.h"
|
||||
#include "db/DeviceInfo.h"
|
||||
#include "device/DeviceWidget.h"
|
||||
#include "dao/DeviceDAO.h"
|
||||
@ -96,13 +101,19 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
|
||||
m_eventWindow->activateWindow();
|
||||
});
|
||||
|
||||
connect(fileLogAction, &QAction::triggered, this, [=]() {
|
||||
if (!m_fileLogWindow) {
|
||||
m_fileLogWindow = new FileLogWindow(this);
|
||||
connect(fileLogAction, &QAction::triggered, this, [this]() {
|
||||
QMessageBox msgBox(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()) {
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include "TutorialWindow.h"
|
||||
#include "bank/AccountWindow.h"
|
||||
#include "db/DeviceInfo.h"
|
||||
#include "log/FileLogWindow.h"
|
||||
#include "widget/common/FlowLayout.h"
|
||||
|
||||
class MainWindow final : public QMainWindow {
|
||||
@ -32,7 +33,7 @@ private:
|
||||
QWidget *m_accountsWindow = nullptr;
|
||||
QWidget *m_tutorialWindow = nullptr;
|
||||
QWidget *m_bankListWindow = nullptr;
|
||||
QWidget *m_fileLogWindow = nullptr;
|
||||
FileLogWindow *m_fileLogWindow = nullptr;
|
||||
QWidget *m_eventWindow = nullptr;
|
||||
|
||||
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, worker, &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);
|
||||
worker->moveToThread(thread);
|
||||
connect(thread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start);
|
||||
|
||||
@ -398,7 +398,7 @@ void BankSetupWizard::runPinCheck() {
|
||||
}, Qt::QueuedConnection);
|
||||
connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
|
||||
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);
|
||||
worker->moveToThread(m_workerThread);
|
||||
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::finished, worker, &QObject::deleteLater);
|
||||
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);
|
||||
worker->moveToThread(m_workerThread);
|
||||
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::finished, worker, &QObject::deleteLater);
|
||||
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);
|
||||
worker->moveToThread(m_workerThread);
|
||||
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]() {
|
||||
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);
|
||||
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||
connect(wizard, &QDialog::finished, this, [this, tableWidget]() {
|
||||
|
||||
@ -239,7 +239,7 @@ void DeviceWidget::setOpenBanksSettings(QPushButton *button, const BankProfileIn
|
||||
QString deviceName = m_device.name;
|
||||
connect(button, &QPushButton::clicked, this, [deviceId, deviceName, app]() {
|
||||
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);
|
||||
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
||||
wizard->show();
|
||||
|
||||
@ -1,15 +1,23 @@
|
||||
#include "EventWindow.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QHttpMultiPart>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QProcess>
|
||||
#include <QThread>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "dao/EventDAO.h"
|
||||
#include "dao/SettingsDAO.h"
|
||||
#include "net/NetworkService.h"
|
||||
|
||||
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_JSON = 8;
|
||||
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) {
|
||||
QJsonObject errData;
|
||||
@ -71,15 +80,15 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
|
||||
|
||||
// ── Таблица ───────────────────────────────────────────────────────────────
|
||||
m_table = new QTableWidget(this);
|
||||
m_table->setColumnCount(11);
|
||||
m_table->setColumnCount(12);
|
||||
m_table->setHorizontalHeaderLabels({
|
||||
"#", "Время", "Тип", "Банк", "Устройство", "Статус", "Сумма", "Комментарий", "JSON", "Отпр.", ""
|
||||
"#", "Время", "Тип", "Банк", "Устройство", "Статус", "Сумма", "Комментарий", "JSON", "Отпр.", "Скрин", ""
|
||||
});
|
||||
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_table->verticalHeader()->hide();
|
||||
m_table->horizontalHeader()->setStretchLastSection(true);
|
||||
m_table->horizontalHeader()->setStretchLastSection(false);
|
||||
m_table->setColumnWidth(COL_ID, 45);
|
||||
m_table->setColumnWidth(COL_TIME, 145);
|
||||
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_STATUS, 100);
|
||||
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_ACTION, 80);
|
||||
m_table->setColumnWidth(COL_IMG, 45);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(COL_COMMENT, QHeaderView::Stretch);
|
||||
|
||||
// ── Панель скриншота ──────────────────────────────────────────────────────
|
||||
m_screenshotLabel = new QLabel(this);
|
||||
@ -112,6 +123,8 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
|
||||
m_pageLabel->setMinimumWidth(140);
|
||||
|
||||
auto *refreshBtn = new QPushButton("Обновить", this);
|
||||
m_sendBtn = new QPushButton("Отправить", this);
|
||||
m_sendBtn->setStyleSheet("background-color: #0088cc; color: white; border-radius: 4px; padding: 4px 12px;");
|
||||
|
||||
auto *paginationLayout = new QHBoxLayout;
|
||||
paginationLayout->addWidget(m_prevBtn);
|
||||
@ -119,6 +132,7 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
|
||||
paginationLayout->addWidget(m_nextBtn);
|
||||
paginationLayout->addStretch();
|
||||
paginationLayout->addWidget(refreshBtn);
|
||||
paginationLayout->addWidget(m_sendBtn);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addWidget(splitter, 1);
|
||||
@ -140,6 +154,8 @@ EventWindow::EventWindow(QWidget *parent) : QDialog(parent) {
|
||||
showScreenshot(m_eventIds[row]);
|
||||
}
|
||||
});
|
||||
connect(m_sendBtn, &QPushButton::clicked, this, &EventWindow::sendEventsToCsv);
|
||||
|
||||
connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int col) {
|
||||
if (col != COL_JSON) return;
|
||||
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_SENT, new QTableWidgetItem(e.sentToServer ? "+" : ""));
|
||||
m_table->setItem(i, COL_IMG, new QTableWidgetItem(e.hasScreenshot ? "+" : ""));
|
||||
|
||||
// Подсветка строки по статусу
|
||||
QColor bg;
|
||||
@ -247,3 +264,138 @@ void EventWindow::updatePagination() {
|
||||
m_prevBtn->setEnabled(m_page > 0);
|
||||
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;
|
||||
QPushButton *m_prevBtn;
|
||||
QPushButton *m_nextBtn;
|
||||
QPushButton *m_sendBtn;
|
||||
|
||||
int m_page = 0;
|
||||
int m_pageSize = 50;
|
||||
@ -28,4 +29,5 @@ private:
|
||||
void loadPage();
|
||||
void updatePagination();
|
||||
void showScreenshot(int eventId);
|
||||
void sendEventsToCsv();
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ class FileLogWindow final : public QDialog {
|
||||
|
||||
public:
|
||||
explicit FileLogWindow(QWidget *parent = nullptr);
|
||||
void sendLogToTelegram();
|
||||
|
||||
private:
|
||||
QTableWidget *m_table;
|
||||
@ -33,5 +34,4 @@ private:
|
||||
void loadPage();
|
||||
void updatePagination();
|
||||
void showScreenshot(int row);
|
||||
void sendLogToTelegram();
|
||||
};
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
#include <QVBoxLayout>
|
||||
#include <QTextBrowser>
|
||||
#include <QEvent>
|
||||
#include <QCoreApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QRegularExpression>
|
||||
#include <QApplication>
|
||||
|
||||
CollapsibleSection::CollapsibleSection(
|
||||
@ -26,7 +28,20 @@ CollapsibleSection::CollapsibleSection(
|
||||
|
||||
// Контент без собственного скрола, займёт доступное пространство
|
||||
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->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_contentBrowser->setFrameShape(QFrame::NoFrame);
|
||||
|
||||