480 lines
20 KiB
C++
480 lines
20 KiB
C++
#include "PayByPhoneScript.h"
|
||
|
||
#include <QProcess>
|
||
#include <QRegularExpression>
|
||
#include <QThread>
|
||
#include <QTimeZone>
|
||
#include <utility>
|
||
|
||
#include <tesseract/baseapi.h>
|
||
#include <leptonica/allheaders.h>
|
||
|
||
#include "adb/AdbUtils.h"
|
||
#include "dao/MaterialDAO.h"
|
||
#include "dao/BankProfileDAO.h"
|
||
#include "dao/DeviceDAO.h"
|
||
#include "dao/TransactionDAO.h"
|
||
#include "time/DateUtils.h"
|
||
|
||
static QString recognizeTextFromImage(const QByteArray &pngData) {
|
||
auto *api = new tesseract::TessBaseAPI();
|
||
if (api->Init(nullptr, "rus+eng")) {
|
||
qWarning() << "[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) {
|
||
qWarning() << "[OCR] Failed to read image";
|
||
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;
|
||
}
|
||
|
||
static QString extractTransactionId(const QString &ocrText) {
|
||
// "ID операции A60130542076330K0000040011670301" или "ID операции = Аб60650..."
|
||
QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-zА-Яа-я0-9]+))");
|
||
if (auto m = reId.match(ocrText); m.hasMatch()) return m.captured(1);
|
||
|
||
// "№ Ф-2026-25235663"
|
||
QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))");
|
||
if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1);
|
||
|
||
return {};
|
||
}
|
||
|
||
namespace Ozon {
|
||
|
||
PayByPhoneScript::PayByPhoneScript(
|
||
MaterialInfo account,
|
||
QString phone,
|
||
QString bankName,
|
||
const double amount,
|
||
QObject *parent
|
||
) : CommonScript(parent),
|
||
m_account(std::move(account)), m_phone(std::move(phone)),
|
||
m_bankName(std::move(bankName)), 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.id, ++m_counter));
|
||
|
||
// Закрываем рекламный bottom sheet
|
||
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
|
||
qDebug() << "[PayByPhone] Ad banner detected, closing...";
|
||
AdbUtils::makeTap(device.id, adNode.x(), adNode.y());
|
||
QThread::msleep(1000);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
|
||
}
|
||
|
||
// Если не на домашнем экране — перезапускаем
|
||
if (!xmlScreenParser.isHomeScreen()) {
|
||
qDebug() << "[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;
|
||
AdbUtils::tryToKillApplication(device.adbSerial, app.package);
|
||
emit finishedWithResult(m_error);
|
||
return;
|
||
}
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
|
||
}
|
||
|
||
// Закрываем баннеры
|
||
tryToCloseAllBannersOnHomePage(device.id, device.screenWidth, device.screenHeight);
|
||
QThread::msleep(500);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
|
||
|
||
// Закрываем баннер Google Play "Доступно обновление"
|
||
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(); !gpNode.isEmpty()) {
|
||
qDebug() << "[PayByPhone] Google Play update banner detected, closing...";
|
||
AdbUtils::makeTap(device.id, gpNode.x(), gpNode.y());
|
||
QThread::msleep(1500);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
|
||
}
|
||
|
||
// Закрываем баннер "Заказать карту" — тапаем сверху экрана
|
||
if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) {
|
||
qDebug() << "[PayByPhone] 'Заказать карту' banner detected, closing...";
|
||
AdbUtils::makeTap(device.id, device.screenWidth / 2, device.screenHeight / 20);
|
||
QThread::msleep(1000);
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++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 transferBtn = xmlScreenParser.findButtonNode("Перевести", false);
|
||
if (transferBtn.isEmpty()) {
|
||
m_error = "Кнопка 'Перевести' не найдена на домашнем экране";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
qDebug() << "[PayByPhone] Tapping 'Перевести'...";
|
||
AdbUtils::makeTap(deviceId, transferBtn.x(), transferBtn.y());
|
||
QThread::msleep(2000);
|
||
|
||
// 2. Ждём экран "Платежи и переводы" с полем "Телефон получателя"
|
||
bool paymentsPageLoaded = false;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
Node phoneField = xmlScreenParser.findFirstEditText();
|
||
if (!phoneField.isEmpty()) {
|
||
paymentsPageLoaded = true;
|
||
break;
|
||
}
|
||
qDebug() << "[PayByPhone] Waiting for payments page, attempt" << attempt + 1 << "/ 10";
|
||
QThread::msleep(1500);
|
||
}
|
||
|
||
if (!paymentsPageLoaded) {
|
||
m_error = "Экран 'Платежи и переводы' не загрузился";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
// 3. Тапаем на поле "Телефон получателя" чтобы перейти на экран "Кому перевести"
|
||
Node phoneFieldTap = xmlScreenParser.findFirstEditText();
|
||
qDebug() << "[PayByPhone] Tapping 'Телефон получателя'...";
|
||
AdbUtils::makeTap(deviceId, phoneFieldTap.x(), phoneFieldTap.y());
|
||
QThread::msleep(2000);
|
||
|
||
// 4. Ждём экран "Кому перевести"
|
||
bool phoneScreenLoaded = false;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
Node title = xmlScreenParser.findTextNode("Кому перевести");
|
||
if (!title.isEmpty()) {
|
||
phoneScreenLoaded = true;
|
||
break;
|
||
}
|
||
qDebug() << "[PayByPhone] Waiting for 'Кому перевести', attempt" << attempt + 1 << "/ 10";
|
||
QThread::msleep(1500);
|
||
}
|
||
|
||
if (!phoneScreenLoaded) {
|
||
m_error = "Экран 'Кому перевести' не загрузился";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
// 5. Тапаем на EditText и вводим номер телефона
|
||
Node phoneInput;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
phoneInput = xmlScreenParser.findFirstEditText();
|
||
if (!phoneInput.isEmpty()) break;
|
||
qDebug() << "[PayByPhone] Waiting for phone input field, attempt" << attempt + 1 << "/ 10";
|
||
QThread::msleep(1500);
|
||
}
|
||
if (phoneInput.isEmpty()) {
|
||
m_error = "Поле ввода телефона не найдено";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
AdbUtils::makeTap(deviceId, phoneInput.x(), phoneInput.y());
|
||
QThread::msleep(500);
|
||
AdbUtils::inputText(deviceId, m_phone);
|
||
QThread::msleep(2000);
|
||
|
||
// 6. Ждём экран "По номеру телефона", обрабатывая промежуточные экраны
|
||
bool sumScreenLoaded = false;
|
||
bool bankSelected = false;
|
||
bool otherBankClicked = false;
|
||
for (int attempt = 0; attempt < 20; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
|
||
// Проверяем: уже на экране суммы?
|
||
Node sumTitle = xmlScreenParser.findTextNode("По номеру телефона");
|
||
if (!sumTitle.isEmpty() && sumTitle.y() < height / 7) {
|
||
sumScreenLoaded = true;
|
||
break;
|
||
}
|
||
|
||
// Ищем кнопку с номером телефона (новый перевод / последние переводы)
|
||
QString phoneDigits = m_phone;
|
||
phoneDigits.remove(QRegularExpression("[^0-9]"));
|
||
bool foundPhoneBtn = false;
|
||
for (const Node &btn : xmlScreenParser.findAllButtons()) {
|
||
QString btnDigits = btn.text;
|
||
btnDigits.remove(QRegularExpression("[^0-9]"));
|
||
if (btnDigits.contains(phoneDigits) || phoneDigits.contains(btnDigits.right(10))) {
|
||
qDebug() << "[PayByPhone] Found button with phone number:" << btn.text;
|
||
AdbUtils::makeTap(deviceId, btn.x(), btn.y());
|
||
QThread::msleep(2000);
|
||
foundPhoneBtn = true;
|
||
break;
|
||
}
|
||
}
|
||
if (foundPhoneBtn) continue;
|
||
|
||
// Экран выбора банка: ищем "Другой банк" (текст) или EditText с hint "Название банка"
|
||
if (!bankSelected) {
|
||
// Вариант 1: кнопка "Другой банк" (content-desc="other-bank-item", text пустой)
|
||
bool foundOtherBank = false;
|
||
{
|
||
Node node;
|
||
for (const Node &n : xmlScreenParser.findAllNodes()) {
|
||
if (n.contentDesc.contains("other-bank-item")) {
|
||
node = n;
|
||
break;
|
||
}
|
||
}
|
||
if (!node.isEmpty()) {
|
||
qDebug() << "[PayByPhone] Tapping 'Другой банк' (other-bank-item)...";
|
||
AdbUtils::makeTap(deviceId, node.x(), node.y());
|
||
QThread::msleep(2000);
|
||
foundOtherBank = true;
|
||
otherBankClicked = true;
|
||
}
|
||
}
|
||
if (foundOtherBank) continue;
|
||
|
||
// Вариант 2: уже на экране поиска банка (EditText) — только после клика на "Другой банк"
|
||
if (otherBankClicked) {
|
||
Node bankInput = xmlScreenParser.findFirstEditText();
|
||
if (!bankInput.isEmpty()) {
|
||
qDebug() << "[PayByPhone] Bank search screen detected, entering bank name...";
|
||
AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y());
|
||
QThread::msleep(500);
|
||
|
||
// Ввод кириллицы через ADB Keyboard
|
||
AdbUtils::enableAdbIMEKeyboard(deviceId);
|
||
QThread::msleep(500);
|
||
AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y());
|
||
QThread::msleep(500);
|
||
AdbUtils::inputAdbIMEText(deviceId, m_bankName);
|
||
QThread::msleep(2000);
|
||
AdbUtils::disableAdbIMEKeyboard(deviceId);
|
||
qDebug() << "[PayByPhone] Entered bank name via ADB IME:" << m_bankName;
|
||
QThread::msleep(2000);
|
||
|
||
// Ищем банк в списке и кликаем по нему (кнопка с числовым content-desc)
|
||
bool bankFound = false;
|
||
for (int ba = 0; ba < 10; ++ba) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
for (const Node &node : xmlScreenParser.findAllNodes()) {
|
||
if (node.className == "android.widget.Button"
|
||
&& !node.contentDesc.isEmpty()
|
||
&& node.contentDesc != "base-go-back-button"
|
||
&& node.contentDesc != "native-toolbar-close-button"
|
||
&& node.contentDesc != "clear-input-icon") {
|
||
qDebug() << "[PayByPhone] Found bank in list, tapping:" << node.contentDesc << "bounds:" << node.x() << node.y();
|
||
AdbUtils::makeTap(deviceId, node.x(), node.y());
|
||
bankFound = true;
|
||
break;
|
||
}
|
||
}
|
||
if (bankFound) break;
|
||
qDebug() << "[PayByPhone] Waiting for bank list, attempt" << ba + 1 << "/ 10";
|
||
QThread::msleep(1500);
|
||
}
|
||
|
||
if (!bankFound) {
|
||
m_error = "Банк '" + m_bankName + "' не найден в списке";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
bankSelected = true;
|
||
QThread::msleep(2000);
|
||
continue;
|
||
}
|
||
} // otherBankClicked
|
||
}
|
||
|
||
qDebug() << "[PayByPhone] Waiting for 'По номеру телефона', attempt" << attempt + 1 << "/ 20";
|
||
QThread::msleep(1500);
|
||
}
|
||
|
||
if (!sumScreenLoaded) {
|
||
m_error = "Экран ввода суммы не загрузился";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
// 7. Вводим сумму
|
||
Node sumInput = xmlScreenParser.findFirstEditText();
|
||
if (sumInput.isEmpty()) {
|
||
m_error = "Поле ввода суммы не найдено";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
AdbUtils::makeTap(deviceId, sumInput.x(), sumInput.y());
|
||
QThread::msleep(500);
|
||
AdbUtils::inputText(deviceId, QString::number(static_cast<int>(m_amount)));
|
||
QThread::msleep(1000);
|
||
|
||
// 6. Нажимаем "Продолжить"
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
Node continueBtn = xmlScreenParser.findButtonNode("Продолжить", false);
|
||
if (continueBtn.isEmpty()) {
|
||
m_error = "Кнопка 'Продолжить' не найдена";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
AdbUtils::makeTap(deviceId, continueBtn.x(), continueBtn.y());
|
||
QThread::msleep(2000);
|
||
|
||
// 7. Ждём экран "Подтвердите перевод" с кнопкой "Перевести ..."
|
||
bool confirmScreenLoaded = false;
|
||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
Node confirmTitle = xmlScreenParser.findTextNode("Подтвердите перевод");
|
||
if (!confirmTitle.isEmpty()) {
|
||
confirmScreenLoaded = true;
|
||
break;
|
||
}
|
||
qDebug() << "[PayByPhone] Waiting for 'Подтвердите перевод', attempt" << attempt + 1 << "/ 10";
|
||
QThread::msleep(1500);
|
||
}
|
||
|
||
if (!confirmScreenLoaded) {
|
||
m_error = "Экран подтверждения не загрузился";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
// 8. Нажимаем "Перевести ..." (ищем по первому слову)
|
||
Node transferConfirmBtn = xmlScreenParser.findButtonNode("Перевести", true);
|
||
if (transferConfirmBtn.isEmpty()) {
|
||
m_error = "Кнопка 'Перевести ...' не найдена на экране подтверждения";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
qDebug() << "[PayByPhone] Tapping confirm button:" << transferConfirmBtn.text;
|
||
AdbUtils::makeTap(deviceId, transferConfirmBtn.x(), transferConfirmBtn.y());
|
||
QThread::msleep(3000);
|
||
|
||
// 9. Ждём результат: "Успешно" или "Не удалось"
|
||
for (int attempt = 0; attempt < 15; ++attempt) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
|
||
if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) {
|
||
qDebug() << "[PayByPhone] Payment successful!";
|
||
m_error.clear();
|
||
|
||
// Сохраняем транзакцию в БД
|
||
TransactionInfo tx;
|
||
tx.accountId = m_account.id;
|
||
tx.amount = -m_amount;
|
||
tx.phone = m_phone;
|
||
tx.bankName = "ozon";
|
||
tx.type = TransactionType::Phone;
|
||
tx.status = TransactionStatus::Complete;
|
||
tx.timestamp = DateUtils::getUtcNow();
|
||
tx.completeTime = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow"));
|
||
tx.bankTime = tx.completeTime.toString("dd.MM.yyyy, HH:mm");
|
||
const int txId = TransactionDAO::insertTransaction(tx);
|
||
qDebug() << "[PayByPhone] Saved transaction to DB, id:" << txId;
|
||
|
||
// Ищем кнопку "Документы" и достаём ID операции
|
||
Node docBtn = xmlScreenParser.findButtonNode("Документы", false);
|
||
if (!docBtn.isEmpty() && txId != -1) {
|
||
qDebug() << "[PayByPhone] Tapping 'Документы'...";
|
||
AdbUtils::makeTap(deviceId, docBtn.x(), docBtn.y());
|
||
QThread::msleep(2000);
|
||
|
||
// Ждём появления кнопки "Поделиться"
|
||
bool docLoaded = false;
|
||
for (int da = 0; da < 20; ++da) {
|
||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||
Node shareBtn = xmlScreenParser.findNodeByContentDesc("Поделиться");
|
||
if (!shareBtn.isEmpty()) {
|
||
docLoaded = true;
|
||
break;
|
||
}
|
||
qDebug() << "[PayByPhone] Waiting for 'Поделиться', attempt" << da + 1 << "/ 20";
|
||
QThread::msleep(1500);
|
||
}
|
||
|
||
if (docLoaded) {
|
||
QThread::msleep(500);
|
||
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
|
||
if (!screenshot.isEmpty()) {
|
||
TransactionDAO::updateReceiptImage(txId, screenshot);
|
||
qDebug() << "[PayByPhone] Receipt saved to DB, txId:" << txId;
|
||
|
||
const QString ocrText = recognizeTextFromImage(screenshot);
|
||
if (!ocrText.isEmpty()) {
|
||
qDebug() << "[PayByPhone] OCR result:\n" << ocrText;
|
||
const QString externalId = extractTransactionId(ocrText);
|
||
if (!externalId.isEmpty()) {
|
||
TransactionDAO::updateBankTrExternalId(txId, externalId);
|
||
qDebug() << "[PayByPhone] Extracted transaction ID:" << externalId;
|
||
} else {
|
||
qWarning() << "[PayByPhone] Could not extract transaction ID from OCR text";
|
||
}
|
||
} else {
|
||
qWarning() << "[PayByPhone] OCR returned empty text";
|
||
}
|
||
} else {
|
||
qWarning() << "[PayByPhone] Screenshot is empty";
|
||
}
|
||
} else {
|
||
qWarning() << "[PayByPhone] 'Поделиться' not found, skipping receipt";
|
||
}
|
||
|
||
AdbUtils::goBack(deviceId);
|
||
QThread::msleep(1000);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) {
|
||
m_error = "Перевод не удался (экран 'Не удалось')";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
return;
|
||
}
|
||
|
||
qDebug() << "[PayByPhone] Waiting for payment result, attempt" << attempt + 1 << "/ 15";
|
||
QThread::msleep(2000);
|
||
}
|
||
|
||
m_error = "Не дождались результата перевода";
|
||
qWarning() << "[PayByPhone]" << m_error;
|
||
}
|
||
|
||
} // namespace Ozon
|