Introduce TaskDumpRecorder for centralized task dump management:
- Add TaskDumpRecorder to save task-related XML dumps and upload failure info to Telegram. - Integrate TaskDumpRecorder into `PayByPhoneScript` and `PayByCardScript` for Dushanbe and Ozon banks. - Extend ADB utilities to record XML dumps with TaskDumpRecorder and refine keyboard dismissal logic. - Enhance freeze detection with consecutive dump checks in CommonScript and payment scripts. - Update mock server profiles and scenario configurations with new material IDs.
This commit is contained in:
parent
bd13ba6e66
commit
1346ca3576
@ -60,27 +60,27 @@ find_package(Qt6 REQUIRED COMPONENTS
|
||||
Xml
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE DB_SOURCES
|
||||
file(GLOB_RECURSE DB_SOURCES CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/database/*.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/database/*.h"
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE VIEW_SOURCES
|
||||
file(GLOB_RECURSE VIEW_SOURCES CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/views/*.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/views/*.h"
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE SERVICES_SOURCES
|
||||
file(GLOB_RECURSE SERVICES_SOURCES CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/services/*.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/services/*.h"
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE MODELS_SOURCES
|
||||
file(GLOB_RECURSE MODELS_SOURCES CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/models/*.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/models/*.h"
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE ANDROID_SOURCES
|
||||
file(GLOB_RECURSE ANDROID_SOURCES CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/android/*.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/android/*.h"
|
||||
)
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
#include <QRegularExpression>
|
||||
#include <QThread>
|
||||
|
||||
#include "dump/TaskDumpRecorder.h"
|
||||
|
||||
AdbUtils::AdbUtils(QObject *parent) : QObject(parent) {
|
||||
}
|
||||
|
||||
@ -235,9 +237,19 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool scre
|
||||
// 5. Проверяем что XML не пустой, достаточного размера и содержит непустые UI-элементы
|
||||
static const QRegularExpression reNonEmpty(R"((text|content-desc)="[^"]+")");
|
||||
const bool hasContent = reNonEmpty.match(xmlOut).hasMatch();
|
||||
// Дополнительно: ловим "WebView-shell-only" состояние, когда AT отдаёт
|
||||
// только обёртку WebView и нижнюю нав-панель Ozon (FINANCE_TAB*),
|
||||
// но Svelte-контент ещё не отрисован. Признак полезного среза —
|
||||
// хотя бы один Button с непустым text ИЛИ EditText. Нав-табы у Ozon
|
||||
// имеют пустой text (только content-desc), поэтому регекс их не зачтёт.
|
||||
static const QRegularExpression reUsefulUi(
|
||||
R"(class="android\.widget\.Button"[^/]*text="[^"]+"|text="[^"]+"[^/]*class="android\.widget\.Button"|class="android\.widget\.EditText")"
|
||||
);
|
||||
const bool hasUsefulUi = reUsefulUi.match(xmlOut).hasMatch();
|
||||
if (xmlOut.trimmed().isEmpty() || !xmlOut.contains("<?xml")
|
||||
|| xmlOut.size() < 500 || !hasContent) {
|
||||
|| xmlOut.size() < 500 || !hasContent || !hasUsefulUi) {
|
||||
qWarning() << "[AdbUtils] getScreenDumpAsXml: bad dump (size=" << xmlOut.size()
|
||||
<< "useful=" << hasUsefulUi
|
||||
<< "), retry" << (retry + 1) << "of 3";
|
||||
QThread::msleep(1500);
|
||||
continue;
|
||||
@ -256,7 +268,11 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool scre
|
||||
if (const int start = xmlBytes.indexOf("<?xml"); start != -1) {
|
||||
xmlBytes = xmlBytes.mid(start);
|
||||
}
|
||||
return QString::fromUtf8(xmlBytes);
|
||||
const QString cleanXml = QString::fromUtf8(xmlBytes);
|
||||
if (auto *rec = TaskDumpRecorder::current()) {
|
||||
rec->saveXml(idx, cleanXml);
|
||||
}
|
||||
return cleanXml;
|
||||
}
|
||||
|
||||
qWarning() << "[AdbUtils] getScreenDumpAsXml: empty dump after 3 retries";
|
||||
@ -503,19 +519,40 @@ bool AdbUtils::inputText(const QString &deviceId, const QString &text) {
|
||||
}
|
||||
|
||||
bool AdbUtils::hideKeyboard(const QString &deviceId) {
|
||||
QProcess process;
|
||||
process.setWorkingDirectory(adbDir());
|
||||
process.start(adbPath(), {
|
||||
"-s", deviceId,
|
||||
"shell", "input",
|
||||
"keyevent", "111"
|
||||
});
|
||||
if (!process.waitForStarted(5000)) {
|
||||
qWarning() << "[AdbUtils] Failed to start adb keyevent:" << process.errorString();
|
||||
return false;
|
||||
// Метод 1: KEYCODE_ESCAPE (111) — работает на большинстве Android-устройств,
|
||||
// но игнорируется на некоторых прошивках (например, TECNO/HiOS).
|
||||
{
|
||||
QProcess p;
|
||||
p.setWorkingDirectory(adbDir());
|
||||
p.start(adbPath(), {"-s", deviceId, "shell", "input", "keyevent", "111"});
|
||||
if (p.waitForStarted(5000)) p.waitForFinished(3000);
|
||||
}
|
||||
process.waitForFinished();
|
||||
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
|
||||
// Метод 2: KEYCODE_BACK (4) — Android посылает BACK сначала в IME; если
|
||||
// IME (клавиатура) видим — он его потребляет и закрывает, не уходя
|
||||
// на предыдущий экран. На TECNO/HiOS это часто единственный рабочий
|
||||
// вариант. Если клавиатура уже скрыта (после метода 1), BACK уйдёт
|
||||
// в Activity — нежелательно. Поэтому сначала проверим через dumpsys
|
||||
// input_method, видна ли всё ещё клавиатура.
|
||||
bool imeVisible = false;
|
||||
{
|
||||
QProcess p;
|
||||
p.setWorkingDirectory(adbDir());
|
||||
p.start(adbPath(), {"-s", deviceId, "shell", "dumpsys", "input_method"});
|
||||
if (p.waitForStarted(5000) && p.waitForFinished(5000)) {
|
||||
const QString out = QString::fromUtf8(p.readAllStandardOutput());
|
||||
imeVisible = out.contains(QStringLiteral("mInputShown=true"))
|
||||
|| out.contains(QStringLiteral("mServedInputConnection=")) // fallback heuristic
|
||||
&& !out.contains(QStringLiteral("mInputShown=false"));
|
||||
}
|
||||
}
|
||||
if (imeVisible) {
|
||||
qDebug() << "[AdbUtils] hideKeyboard: ESC didn't dismiss IME, trying BACK";
|
||||
QProcess p;
|
||||
p.setWorkingDirectory(adbDir());
|
||||
p.start(adbPath(), {"-s", deviceId, "shell", "input", "keyevent", "4"});
|
||||
if (p.waitForStarted(5000)) p.waitForFinished(3000);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AdbUtils::goBack(const QString &deviceId) {
|
||||
|
||||
261
android/dump/TaskDumpRecorder.cpp
Normal file
261
android/dump/TaskDumpRecorder.cpp
Normal file
@ -0,0 +1,261 @@
|
||||
#include "TaskDumpRecorder.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextStream>
|
||||
#include <QThread>
|
||||
#include <QThreadStorage>
|
||||
#include <qDebug>
|
||||
|
||||
#include "adb/AdbUtils.h"
|
||||
|
||||
namespace {
|
||||
|
||||
QThreadStorage<TaskDumpRecorder *> &tlsRecorder() {
|
||||
static QThreadStorage<TaskDumpRecorder *> tls;
|
||||
return tls;
|
||||
}
|
||||
|
||||
constexpr const char *kBotToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8";
|
||||
constexpr const char *kChatId = "-5177086220";
|
||||
|
||||
QString dumpsRoot() {
|
||||
return QCoreApplication::applicationDirPath() + "/dumps";
|
||||
}
|
||||
|
||||
QString sanitize(const QString &raw, int maxLen = 40) {
|
||||
static const QRegularExpression re(QStringLiteral("[^\\p{L}\\p{N}_-]+"));
|
||||
QString s = raw;
|
||||
s.replace(re, "_");
|
||||
if (s.size() > maxLen) s = s.left(maxLen);
|
||||
while (s.endsWith('_')) s.chop(1);
|
||||
while (s.startsWith('_')) s = s.mid(1);
|
||||
return s;
|
||||
}
|
||||
|
||||
QString extractScreenTag(const QString &xml) {
|
||||
static const QRegularExpression re(QStringLiteral("text=\"([^\"]{4,40})\""));
|
||||
static const QRegularExpression noiseRe(QStringLiteral("^[0-9\\s,\\.\\-+₽$€%]+$"));
|
||||
auto it = re.globalMatch(xml);
|
||||
while (it.hasNext()) {
|
||||
QString text = it.next().captured(1).trimmed();
|
||||
if (text.isEmpty() || noiseRe.match(text).hasMatch()) continue;
|
||||
QString tag = sanitize(text);
|
||||
if (!tag.isEmpty()) return tag;
|
||||
}
|
||||
return QStringLiteral("screen");
|
||||
}
|
||||
|
||||
bool runSync(const QString &program, const QStringList &args, int timeoutMs = 180000) {
|
||||
QProcess p;
|
||||
p.start(program, args);
|
||||
if (!p.waitForStarted(5000)) {
|
||||
qWarning() << "[TaskDumpRecorder] failed to start:" << program << args
|
||||
<< p.errorString();
|
||||
return false;
|
||||
}
|
||||
if (!p.waitForFinished(timeoutMs)) {
|
||||
qWarning() << "[TaskDumpRecorder] timeout:" << program << args;
|
||||
p.kill();
|
||||
return false;
|
||||
}
|
||||
if (p.exitCode() != 0) {
|
||||
qWarning() << "[TaskDumpRecorder]" << program << "exit=" << p.exitCode()
|
||||
<< "stderr:" << QString::fromUtf8(p.readAllStandardError()).trimmed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool zipFolder(const QString &folder, const QString &archivePath) {
|
||||
#ifdef Q_OS_WIN
|
||||
const QString cmd = QString(
|
||||
"Compress-Archive -Path '%1\\*' -DestinationPath '%2' -Force"
|
||||
).arg(QDir::toNativeSeparators(folder), QDir::toNativeSeparators(archivePath));
|
||||
return runSync("powershell", {"-NoProfile", "-Command", cmd});
|
||||
#else
|
||||
const QFileInfo fi(folder);
|
||||
QProcess p;
|
||||
p.setWorkingDirectory(fi.absolutePath());
|
||||
p.start("zip", {"-rq", archivePath, fi.fileName()});
|
||||
if (!p.waitForStarted(5000) || !p.waitForFinished(180000)) {
|
||||
qWarning() << "[TaskDumpRecorder] zip failed:" << p.errorString();
|
||||
return false;
|
||||
}
|
||||
return p.exitCode() == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void uploadToTelegram(const QString &archivePath, const QString &caption) {
|
||||
const QString url = QString("https://api.telegram.org/bot%1/sendDocument").arg(kBotToken);
|
||||
const QStringList args = {
|
||||
"-sS", "--max-time", "120",
|
||||
"-F", QString("chat_id=%1").arg(kChatId),
|
||||
"-F", QString("document=@%1").arg(archivePath),
|
||||
"-F", QString("caption=%1").arg(caption),
|
||||
url
|
||||
};
|
||||
runSync("curl", args, 150000);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TaskDumpRecorder *TaskDumpRecorder::current() {
|
||||
return tlsRecorder().hasLocalData() ? tlsRecorder().localData() : nullptr;
|
||||
}
|
||||
|
||||
void TaskDumpRecorder::begin(const QString &bank, const QString &taskKind,
|
||||
const QString &adbDeviceId, const TaskDumpMeta &meta) {
|
||||
if (auto *prev = current()) {
|
||||
prev->finishInternal(QStringLiteral("interrupted (new task started before previous finished)"));
|
||||
// Не вызываем `delete prev` — QThreadStorage<T*> сам удалит предыдущий
|
||||
// указатель при замене через setLocalData.
|
||||
}
|
||||
auto *rec = new TaskDumpRecorder(bank, taskKind, adbDeviceId, meta);
|
||||
tlsRecorder().setLocalData(rec);
|
||||
}
|
||||
|
||||
void TaskDumpRecorder::finish(const QString &errorMessage) {
|
||||
auto *rec = current();
|
||||
if (!rec) return;
|
||||
rec->finishInternal(errorMessage);
|
||||
// setLocalData(nullptr) сам вызовет delete на текущем указателе.
|
||||
tlsRecorder().setLocalData(nullptr);
|
||||
}
|
||||
|
||||
TaskDumpRecorder::TaskDumpRecorder(const QString &bank, const QString &taskKind,
|
||||
const QString &adbDeviceId, const TaskDumpMeta &meta)
|
||||
: m_bank(sanitize(bank)),
|
||||
m_taskKind(sanitize(taskKind)),
|
||||
m_adbDeviceIdRaw(adbDeviceId),
|
||||
m_adbDeviceIdSafe(sanitize(adbDeviceId)),
|
||||
m_meta(meta) {
|
||||
// Короткий суффикс для имени папки — чтобы визуально отличать задачи,
|
||||
// но не раздувать путь. Берём materialId (первые 8 символов — UUID segment)
|
||||
// + сумму, если есть.
|
||||
QString folderTail;
|
||||
if (!m_meta.materialId.isEmpty()) {
|
||||
folderTail = sanitize(m_meta.materialId.left(8));
|
||||
}
|
||||
if (m_meta.amount != 0) {
|
||||
const QString amt = QString::number(static_cast<qint64>(m_meta.amount));
|
||||
folderTail = folderTail.isEmpty() ? amt : folderTail + "_" + amt;
|
||||
}
|
||||
|
||||
const QString ts = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
|
||||
const QString sub = folderTail.isEmpty()
|
||||
? QString("%1_%2").arg(ts, m_adbDeviceIdSafe)
|
||||
: QString("%1_%2_%3").arg(ts, m_adbDeviceIdSafe, folderTail);
|
||||
m_folder = QString("%1/%2_%3/%4")
|
||||
.arg(dumpsRoot(), m_bank, m_taskKind, sub);
|
||||
if (!QDir().mkpath(m_folder)) {
|
||||
qWarning() << "[TaskDumpRecorder] failed to mkpath" << m_folder;
|
||||
m_folder.clear();
|
||||
return;
|
||||
}
|
||||
qDebug() << "[TaskDumpRecorder] begin:" << m_folder;
|
||||
}
|
||||
|
||||
QString TaskDumpRecorder::buildCaption(const QString &errorMessage) const {
|
||||
QString s = QString("Bank: %1 | Task: %2 | Device: %3")
|
||||
.arg(m_bank, m_taskKind, m_adbDeviceIdRaw);
|
||||
if (!m_meta.materialId.isEmpty()) s += QString("\nMaterial ID: %1").arg(m_meta.materialId);
|
||||
if (m_meta.amount != 0) s += QString("\nAmount: %1").arg(static_cast<qint64>(m_meta.amount));
|
||||
if (!m_meta.phone.isEmpty()) s += QString("\nPhone: %1").arg(m_meta.phone);
|
||||
if (!m_meta.card.isEmpty()) s += QString("\nCard: %1").arg(m_meta.card);
|
||||
s += QString("\nError: %1").arg(errorMessage.left(800));
|
||||
return s;
|
||||
}
|
||||
|
||||
void TaskDumpRecorder::saveXml(int counter, const QString &xml) {
|
||||
if (m_folder.isEmpty() || xml.isEmpty()) return;
|
||||
const QString tag = extractScreenTag(xml);
|
||||
const QString fname = QString("%1/%2_%3.xml")
|
||||
.arg(m_folder,
|
||||
QString("%1").arg(counter, 4, 10, QChar('0')),
|
||||
tag);
|
||||
QFile f(fname);
|
||||
if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
f.write(xml.toUtf8());
|
||||
f.close();
|
||||
} else {
|
||||
qWarning() << "[TaskDumpRecorder] failed to write" << fname << f.errorString();
|
||||
}
|
||||
}
|
||||
|
||||
void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
|
||||
if (m_finished) return;
|
||||
m_finished = true;
|
||||
if (m_folder.isEmpty()) return;
|
||||
|
||||
if (errorMessage.isEmpty()) {
|
||||
QDir(m_folder).removeRecursively();
|
||||
qDebug() << "[TaskDumpRecorder] success — removed" << m_folder;
|
||||
return;
|
||||
}
|
||||
|
||||
qWarning() << "[TaskDumpRecorder] task failed:" << errorMessage
|
||||
<< "folder:" << m_folder;
|
||||
|
||||
// Финальный скриншот текущего экрана
|
||||
if (!m_adbDeviceIdRaw.isEmpty()) {
|
||||
const QByteArray png = AdbUtils::captureScreenshotBytes(m_adbDeviceIdRaw);
|
||||
if (!png.isEmpty()) {
|
||||
QFile f(m_folder + "/_final_screen.png");
|
||||
if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
f.write(png);
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Метаданные
|
||||
{
|
||||
QFile ef(m_folder + "/_error.txt");
|
||||
if (ef.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
QTextStream ts(&ef);
|
||||
ts.setEncoding(QStringConverter::Utf8);
|
||||
ts << "bank: " << m_bank << "\n"
|
||||
<< "task: " << m_taskKind << "\n"
|
||||
<< "device: " << m_adbDeviceIdRaw << "\n";
|
||||
if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n";
|
||||
if (m_meta.amount != 0) ts << "amount: " << static_cast<qint64>(m_meta.amount) << "\n";
|
||||
if (!m_meta.phone.isEmpty()) ts << "phone: " << m_meta.phone << "\n";
|
||||
if (!m_meta.card.isEmpty()) ts << "card: " << m_meta.card << "\n";
|
||||
ts << "time: " << QDateTime::currentDateTime().toString(Qt::ISODate) << "\n"
|
||||
<< "error: " << errorMessage << "\n";
|
||||
ef.close();
|
||||
}
|
||||
}
|
||||
|
||||
const QString folder = m_folder;
|
||||
const QString archivePath = m_folder + ".zip";
|
||||
const QString caption = buildCaption(errorMessage);
|
||||
|
||||
QThread *bg = QThread::create([folder, archivePath, caption]() {
|
||||
if (zipFolder(folder, archivePath)) {
|
||||
uploadToTelegram(archivePath, caption);
|
||||
} else {
|
||||
qWarning() << "[TaskDumpRecorder] zip failed, skipping Telegram upload for" << folder;
|
||||
}
|
||||
QDir(folder).removeRecursively();
|
||||
QFile::remove(archivePath);
|
||||
});
|
||||
QObject::connect(bg, &QThread::finished, bg, &QObject::deleteLater);
|
||||
bg->start();
|
||||
}
|
||||
|
||||
TaskDumpRecorder::Scope::Scope(const QString &bank, const QString &taskKind,
|
||||
const QString &adbDeviceId, const TaskDumpMeta &meta,
|
||||
const QString *errorPtr)
|
||||
: m_errorPtr(errorPtr) {
|
||||
TaskDumpRecorder::begin(bank, taskKind, adbDeviceId, meta);
|
||||
}
|
||||
|
||||
TaskDumpRecorder::Scope::~Scope() {
|
||||
TaskDumpRecorder::finish(m_errorPtr ? *m_errorPtr : QString());
|
||||
}
|
||||
54
android/dump/TaskDumpRecorder.h
Normal file
54
android/dump/TaskDumpRecorder.h
Normal file
@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
// Мета-поля задачи. Пустые/нулевые значения не попадают в caption Telegram
|
||||
// и в имя папки — выводятся только те, что релевантны конкретному типу задачи.
|
||||
struct TaskDumpMeta {
|
||||
QString materialId;
|
||||
double amount = 0; // 0 = нет
|
||||
QString phone; // пусто = нет
|
||||
QString card; // пусто = нет
|
||||
};
|
||||
|
||||
class TaskDumpRecorder {
|
||||
public:
|
||||
// Возвращает рекордер текущего потока (nullptr если задача не начата).
|
||||
static TaskDumpRecorder* current();
|
||||
|
||||
// RAII-скоуп: при создании регистрирует рекордер на текущий поток,
|
||||
// при разрушении — финализирует (успех/ошибка определяются по errorPtr).
|
||||
class Scope {
|
||||
public:
|
||||
Scope(const QString &bank, const QString &taskKind,
|
||||
const QString &adbDeviceId, const TaskDumpMeta &meta,
|
||||
const QString *errorPtr);
|
||||
~Scope();
|
||||
Scope(const Scope &) = delete;
|
||||
Scope &operator=(const Scope &) = delete;
|
||||
private:
|
||||
const QString *m_errorPtr;
|
||||
};
|
||||
|
||||
// Сохраняет XML-дамп в папку задачи. Имя файла: NNNN_<screenTag>.xml.
|
||||
void saveXml(int counter, const QString &xml);
|
||||
|
||||
private:
|
||||
TaskDumpRecorder(const QString &bank, const QString &taskKind,
|
||||
const QString &adbDeviceId, const TaskDumpMeta &meta);
|
||||
|
||||
static void begin(const QString &bank, const QString &taskKind,
|
||||
const QString &adbDeviceId, const TaskDumpMeta &meta);
|
||||
static void finish(const QString &errorMessage);
|
||||
|
||||
void finishInternal(const QString &errorMessage);
|
||||
QString buildCaption(const QString &errorMessage) const;
|
||||
|
||||
QString m_folder;
|
||||
QString m_bank;
|
||||
QString m_taskKind;
|
||||
QString m_adbDeviceIdRaw;
|
||||
QString m_adbDeviceIdSafe;
|
||||
TaskDumpMeta m_meta;
|
||||
bool m_finished = false;
|
||||
};
|
||||
@ -9,6 +9,7 @@
|
||||
#include <leptonica/allheaders.h>
|
||||
|
||||
#include "adb/AdbUtils.h"
|
||||
#include "dump/TaskDumpRecorder.h"
|
||||
#include "dao/MaterialDAO.h"
|
||||
#include "dao/BankProfileDAO.h"
|
||||
#include "dao/DeviceDAO.h"
|
||||
@ -231,12 +232,14 @@ GetLastTransactionsScript::GetLastTransactionsScript(
|
||||
QString appCode,
|
||||
double searchAmount,
|
||||
QDateTime searchTime,
|
||||
QString materialId,
|
||||
QObject *parent
|
||||
) : CommonScript(parent),
|
||||
m_deviceId(std::move(deviceId)),
|
||||
m_appCode(std::move(appCode)),
|
||||
m_searchAmount(searchAmount),
|
||||
m_searchTime(std::move(searchTime)) {
|
||||
m_searchTime(std::move(searchTime)),
|
||||
m_materialId(std::move(materialId)) {
|
||||
}
|
||||
|
||||
GetLastTransactionsScript::~GetLastTransactionsScript() = default;
|
||||
@ -255,6 +258,9 @@ void GetLastTransactionsScript::doStart() {
|
||||
return;
|
||||
}
|
||||
|
||||
const TaskDumpMeta dumpMeta{m_materialId, m_searchAmount, {}, {}};
|
||||
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
|
||||
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
|
||||
if (!xmlScreenParser.isHomeScreen()) {
|
||||
|
||||
@ -16,6 +16,7 @@ public:
|
||||
QString appCode,
|
||||
double searchAmount = 0,
|
||||
QDateTime searchTime = {},
|
||||
QString materialId = {},
|
||||
QObject *parent = nullptr
|
||||
);
|
||||
|
||||
@ -35,6 +36,7 @@ private:
|
||||
const QString m_appCode;
|
||||
const double m_searchAmount; // сумма в сомони (0 = все последние)
|
||||
const QDateTime m_searchTime; // примерное время (UTC)
|
||||
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <leptonica/allheaders.h>
|
||||
|
||||
#include "adb/AdbUtils.h"
|
||||
#include "dump/TaskDumpRecorder.h"
|
||||
#include "dao/BankProfileDAO.h"
|
||||
#include "dao/DeviceDAO.h"
|
||||
#include "dao/TransactionDAO.h"
|
||||
@ -121,6 +122,9 @@ void PayByCardScript::doStart() {
|
||||
return;
|
||||
}
|
||||
|
||||
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber};
|
||||
TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error);
|
||||
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
|
||||
|
||||
if (!xmlScreenParser.isHomeScreen()) {
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <leptonica/allheaders.h>
|
||||
|
||||
#include "adb/AdbUtils.h"
|
||||
#include "dump/TaskDumpRecorder.h"
|
||||
#include "dao/BankProfileDAO.h"
|
||||
#include "dao/DeviceDAO.h"
|
||||
#include "dao/TransactionDAO.h"
|
||||
@ -118,6 +119,9 @@ void PayByPhoneScript::doStart() {
|
||||
return;
|
||||
}
|
||||
|
||||
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}};
|
||||
TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_PHONE", device.adbSerial, dumpMeta, &m_error);
|
||||
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
|
||||
|
||||
if (!xmlScreenParser.isHomeScreen()) {
|
||||
|
||||
@ -447,6 +447,33 @@ bool CommonScript::isAppRunning(const QString &deviceId, const QString &packageN
|
||||
return !AdbUtils::tryToGetRunningAppPid(deviceId, packageName).isEmpty();
|
||||
}
|
||||
|
||||
bool CommonScript::FreezeDetector::isUsefulDump(const QString &xml) {
|
||||
if (xml.isEmpty()) return false;
|
||||
// Полезный дамп должен содержать хотя бы один Button с непустым text
|
||||
// ИЛИ EditText. Иначе — это shell-only state (только нав-табы, у которых
|
||||
// text пуст и есть только content-desc).
|
||||
if (!xml.contains(QStringLiteral("class=\"android.widget.Button\""))) return false;
|
||||
static const QRegularExpression usefulRe(
|
||||
R"(class="android\.widget\.Button"[^/]*text="[^"]+"|text="[^"]+"[^/]*class="android\.widget\.Button"|class="android\.widget\.EditText")"
|
||||
);
|
||||
return usefulRe.match(xml).hasMatch();
|
||||
}
|
||||
|
||||
bool CommonScript::FreezeDetector::noticeAndCheckFrozen(const QString &xml) {
|
||||
if (!isUsefulDump(xml)) {
|
||||
// Пустой или WebView-shell-only — не учитываем.
|
||||
return false;
|
||||
}
|
||||
if (!m_prev.isEmpty() && xml == m_prev) {
|
||||
++m_streak;
|
||||
if (m_streak >= m_maxStreak) return true;
|
||||
} else {
|
||||
m_streak = 0;
|
||||
}
|
||||
m_prev = xml;
|
||||
return false;
|
||||
}
|
||||
|
||||
void CommonScript::stop() {
|
||||
m_running = false;
|
||||
}
|
||||
|
||||
@ -69,6 +69,33 @@ protected:
|
||||
// Проверяет что приложение ещё запущено на устройстве
|
||||
static bool isAppRunning(const QString &deviceId, const QString &packageName);
|
||||
|
||||
// Детектор зафризившегося экрана: считает подряд идущие байт-идентичные
|
||||
// дампы с реальным контентом. Бесполезные дампы (пустые из-за adb-сбоя
|
||||
// или WebView-shell-only без отрисованного Svelte-контента) пропускаются —
|
||||
// не обнуляют счётчик и не накручивают его.
|
||||
//
|
||||
// Использование в wait-loop:
|
||||
// FreezeDetector freezeCheck;
|
||||
// for (int attempt = 0; attempt < N; ++attempt) {
|
||||
// QString xml = AdbUtils::getScreenDumpAsXml(...);
|
||||
// xmlScreenParser.parseAndSaveXml(xml);
|
||||
// if (freezeCheck.noticeAndCheckFrozen(xml)) {
|
||||
// m_error = "Экран зафризился"; return;
|
||||
// }
|
||||
// ... existing logic
|
||||
// }
|
||||
class FreezeDetector {
|
||||
public:
|
||||
explicit FreezeDetector(int maxStreak = 3) : m_maxStreak(maxStreak) {}
|
||||
// True если получили (m_maxStreak + 1) подряд байт-идентичных полезных дампов.
|
||||
bool noticeAndCheckFrozen(const QString &xml);
|
||||
private:
|
||||
static bool isUsefulDump(const QString &xml);
|
||||
QString m_prev;
|
||||
int m_streak = 0;
|
||||
int m_maxStreak;
|
||||
};
|
||||
|
||||
bool m_running = true;
|
||||
ScreenXmlParser xmlScreenParser;
|
||||
int m_counter = 0;
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
#include <leptonica/allheaders.h>
|
||||
|
||||
#include "adb/AdbUtils.h"
|
||||
#include "dump/TaskDumpRecorder.h"
|
||||
#include "dao/MaterialDAO.h"
|
||||
#include "dao/BankProfileDAO.h"
|
||||
#include "dao/DeviceDAO.h"
|
||||
@ -83,13 +84,15 @@ GetLastTransactionsScript::GetLastTransactionsScript(
|
||||
int maxCount,
|
||||
double searchAmount,
|
||||
QDateTime searchTime,
|
||||
QString materialId,
|
||||
QObject *parent
|
||||
) : CommonScript(parent),
|
||||
m_deviceId(std::move(deviceId)),
|
||||
m_appCode(std::move(appCode)),
|
||||
m_maxCount(maxCount),
|
||||
m_searchAmount(searchAmount),
|
||||
m_searchTime(std::move(searchTime)) {
|
||||
m_searchTime(std::move(searchTime)),
|
||||
m_materialId(std::move(materialId)) {
|
||||
}
|
||||
|
||||
GetLastTransactionsScript::~GetLastTransactionsScript() = default;
|
||||
@ -107,6 +110,9 @@ void GetLastTransactionsScript::doStart() {
|
||||
return;
|
||||
}
|
||||
|
||||
const TaskDumpMeta dumpMeta{m_materialId, m_searchAmount, {}, {}};
|
||||
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
|
||||
|
||||
if (!navigateToTransactionList(device, app)) {
|
||||
emit finishedWithResult(m_error);
|
||||
return;
|
||||
@ -245,7 +251,19 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
QTimeZone deviceTz(tzName.toUtf8());
|
||||
if (!deviceTz.isValid()) deviceTz = QTimeZone("Europe/Moscow");
|
||||
const int maxScrolls = 30;
|
||||
const int navBarTop = device.screenHeight - (device.screenHeight / 10);
|
||||
// Реальный y верхней границы нижнего нав-холдера. Если в XML есть
|
||||
// ru.ozon.fintech.finance:id/main_activity_bottom_navigation_holder,
|
||||
// используем его y1; иначе грубая оценка ~10% снизу.
|
||||
auto detectNavBarTop = [&]() -> int {
|
||||
for (const Node &n : xmlScreenParser.getNodes()) {
|
||||
if (n.resourceId == "ru.ozon.fintech.finance:id/main_activity_bottom_navigation_holder"
|
||||
&& n.height() > 0) {
|
||||
return n.y1();
|
||||
}
|
||||
}
|
||||
return device.screenHeight - (device.screenHeight / 10);
|
||||
};
|
||||
int navBarTop = detectNavBarTop();
|
||||
|
||||
// Целевая дата в таймзоне устройства для поиска заголовка дня в списке
|
||||
const QDateTime searchLocal = m_searchTime.toTimeZone(deviceTz);
|
||||
@ -459,10 +477,13 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
return;
|
||||
}
|
||||
|
||||
// Пропускаем кнопки вне Y-диапазона нужной даты
|
||||
// Пропускаем кнопки вне Y-диапазона нужной даты.
|
||||
// Используем y1 (верх кнопки), а не центр — длинные кнопки могут
|
||||
// заходить под нав-бар, но всё равно tappable (тапнем по верхней части).
|
||||
const int btnY1 = txButtons[bi].y1();
|
||||
const int btnY = txButtons[bi].y();
|
||||
if (btnY <= dateHeaderY || btnY >= nextDateHeaderY) continue;
|
||||
if (btnY <= 0 || btnY >= navBarTop) continue;
|
||||
if (btnY1 <= dateHeaderY || btnY1 >= nextDateHeaderY) continue;
|
||||
if (btnY1 <= 0 || btnY1 >= navBarTop) continue;
|
||||
++buttonsInRange;
|
||||
|
||||
const TransactionInfo parsed = xmlScreenParser.parseTransactionButtonText(txButtons[bi].text);
|
||||
@ -472,12 +493,21 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
if (qAbs(parsed.amount - m_searchAmount) > 0.01) continue;
|
||||
|
||||
const Node &visibleBtn = txButtons[bi];
|
||||
// Зажимаем тап-Y на видимую часть кнопки: если кнопка заходит под
|
||||
// нав-бар, центр окажется на нав-баре и тап улетит мимо.
|
||||
const int safeMaxY = navBarTop - 20;
|
||||
int tapY = visibleBtn.y();
|
||||
if (tapY > safeMaxY) {
|
||||
tapY = qMax(visibleBtn.y1() + 30, safeMaxY);
|
||||
}
|
||||
qDebug() << "[FetchTransactions] Amount match:" << parsed.amount
|
||||
<< "at" << visibleBtn.x() << visibleBtn.y()
|
||||
<< "(dateY:" << dateHeaderY << "-" << nextDateHeaderY << ")";
|
||||
<< "at" << visibleBtn.x() << tapY
|
||||
<< "(btn y1=" << visibleBtn.y1() << "y2=" << visibleBtn.y2()
|
||||
<< "navBarTop=" << navBarTop
|
||||
<< "dateY:" << dateHeaderY << "-" << nextDateHeaderY << ")";
|
||||
|
||||
// Тапаем и ждём деталей
|
||||
AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y());
|
||||
AdbUtils::makeTap(m_deviceId, visibleBtn.x(), tapY);
|
||||
QThread::msleep(2000);
|
||||
|
||||
TransactionInfo detail;
|
||||
@ -555,49 +585,13 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
<< "amount=" << parsed.amount << "time=" << dtStr
|
||||
<< "name=" << detail.name;
|
||||
|
||||
// Скриншот экрана деталей транзакции — отдаём в EventHandler.
|
||||
// Раньше дополнительно тапали "Документы" + OCR ради ID операции,
|
||||
// но это медленно и нестабильно. Теперь полагаемся на дату+время+
|
||||
// сумму как идентификатор.
|
||||
m_resultScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId);
|
||||
|
||||
// Пробуем получить ID через "Документы" → OCR
|
||||
QString externalId = detail.bankTrExternalId;
|
||||
if (externalId.isEmpty() && !QThread::currentThread()->isInterruptionRequested()) {
|
||||
Node docBtn = xmlScreenParser.findButtonNode("Документы", false);
|
||||
if (!docBtn.isEmpty()) {
|
||||
qDebug() << "[FetchTransactions] Tapping 'Документы'...";
|
||||
AdbUtils::makeTap(m_deviceId, docBtn.x(), docBtn.y());
|
||||
QThread::msleep(2000);
|
||||
|
||||
bool docLoaded = false;
|
||||
for (int da = 0; da < 5; ++da) {
|
||||
if (QThread::currentThread()->isInterruptionRequested()) break;
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||
if (!xmlScreenParser.findNodeByContentDesc("Поделиться").isEmpty()) {
|
||||
docLoaded = true;
|
||||
break;
|
||||
}
|
||||
QThread::msleep(1500);
|
||||
}
|
||||
|
||||
if (docLoaded) {
|
||||
QThread::msleep(500);
|
||||
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(m_deviceId);
|
||||
if (!screenshot.isEmpty()) {
|
||||
m_resultScreenshot = screenshot;
|
||||
const QString ocrText = recognizeTextFromImage(screenshot);
|
||||
if (!ocrText.isEmpty()) {
|
||||
qDebug() << "[FetchTransactions] OCR result:" << ocrText;
|
||||
externalId = extractTransactionId(ocrText);
|
||||
if (!externalId.isEmpty()) {
|
||||
qDebug() << "[FetchTransactions] Extracted ID:" << externalId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AdbUtils::goBack(m_deviceId);
|
||||
QThread::msleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем
|
||||
if (accountId != -1) {
|
||||
@ -676,39 +670,55 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
|
||||
// Скроллим если не нашли нужную транзакцию
|
||||
if (!tapped || !hasNewButton) {
|
||||
// Признак того, что под фолдом ещё есть не раскрытые транзакции
|
||||
// (Ozon хранит off-screen-row как android.view.View с height=0).
|
||||
const bool moreBelow = xmlScreenParser.hasCollapsedTransactionsBelow(device.screenHeight);
|
||||
qDebug() << "[FetchTransactions] Scrolling (tapped=" << tapped
|
||||
<< "hasNew=" << hasNewButton << "inRange=" << buttonsInRange
|
||||
<< "noProgress=" << noProgressCount
|
||||
<< "dateHeaderY=" << dateHeaderY
|
||||
<< "nextDateHeaderY=" << nextDateHeaderY << ")";
|
||||
<< "nextDateHeaderY=" << nextDateHeaderY
|
||||
<< "moreBelow=" << moreBelow << ")";
|
||||
|
||||
const int cx = device.screenWidth / 2;
|
||||
if (dateHeaderY == 0 && nextDateHeaderY < device.screenHeight) {
|
||||
// Target-header ушёл вверх, под ним видна более старая дата —
|
||||
// скроллим обратно наверх (swipe вниз) чтобы вернуться к target.
|
||||
const int fromY = device.screenHeight / 3;
|
||||
const int toY = device.screenHeight * 2 / 3;
|
||||
qDebug() << "[FetchTransactions] Scroll UP: from" << fromY << "to" << toY;
|
||||
AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, fromY,
|
||||
device.screenWidth / 2, toY, 500);
|
||||
} else if (dateHeaderY > 0 && dateHeaderY < device.screenHeight / 2) {
|
||||
// Заголовок в верхней половине — маленький скролл чтобы показать
|
||||
// больше кнопок под этой датой
|
||||
const int fromY = device.screenHeight * 2 / 3;
|
||||
const int toY = device.screenHeight / 3;
|
||||
qDebug() << "[FetchTransactions] Small scroll down: from" << fromY << "to" << toY
|
||||
<< "(dateHeaderY=" << dateHeaderY << ")";
|
||||
AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, fromY,
|
||||
device.screenWidth / 2, toY, 500);
|
||||
// Target-header ушёл вверх — свайпим вниз чтоб вернуть его.
|
||||
AdbUtils::makeSwipe(m_deviceId, cx,
|
||||
device.screenHeight * 2 / 5, cx,
|
||||
device.screenHeight * 3 / 4, 600);
|
||||
QThread::msleep(1500);
|
||||
} else {
|
||||
// Заголовок в нижней половине или не найден — скролл вниз
|
||||
const int fromY = device.screenHeight * 3 / 4;
|
||||
const int toY = device.screenHeight / 4;
|
||||
qDebug() << "[FetchTransactions] Full scroll down: from" << fromY << "to" << toY
|
||||
<< "(dateHeaderY=" << dateHeaderY << ")";
|
||||
AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, fromY,
|
||||
device.screenWidth / 2, toY, 400);
|
||||
// Скролл вниз. Ozon WebView ведёт себя странно: ОДИН медленный
|
||||
// свайп иногда игнорируется (видимо, расценивается как hold).
|
||||
// Стратегия: серия из 3 коротких быстрых свайпов внутри
|
||||
// безопасной зоны (выше нав-бара). Это надёжно триггерит
|
||||
// нативный fling в WebView.
|
||||
// safeBottom — верхняя граница нав-бара минус запас.
|
||||
// Когда target-секция уже у нижнего края (dateHeaderY >= safeBottom),
|
||||
// зона "внутри секции" схлопывается — свайпаем фиксированным
|
||||
// диапазоном относительно safeBottom, чтобы вытащить контент
|
||||
// секции из-под нав-бара.
|
||||
const int safeBottom = navBarTop - 30;
|
||||
int amplitude;
|
||||
if (dateHeaderY > 0 && dateHeaderY < safeBottom - 100) {
|
||||
// Header виден достаточно высоко — скроллим внутри его секции.
|
||||
const int safeTop = qMax(dateHeaderY + 80, device.screenHeight / 4);
|
||||
amplitude = qMin(safeBottom - safeTop, 700);
|
||||
} else {
|
||||
// Header у самого низа или ниже нав-бара — большой свайп.
|
||||
amplitude = qMin(safeBottom - device.screenHeight / 4, 700);
|
||||
}
|
||||
amplitude = qMax(amplitude, 300); // минимум 300px чтобы было ощутимо
|
||||
qDebug() << "[FetchTransactions] Triple-swipe scroll:"
|
||||
<< "safeBottom=" << safeBottom << "amplitude=" << amplitude
|
||||
<< "(dateHeaderY=" << dateHeaderY << " moreBelow=" << moreBelow << ")";
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
AdbUtils::makeSwipe(m_deviceId, cx, safeBottom,
|
||||
cx, safeBottom - amplitude, 250);
|
||||
QThread::msleep(350);
|
||||
}
|
||||
QThread::msleep(800);
|
||||
}
|
||||
QThread::msleep(1500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ public:
|
||||
int maxCount = 0,
|
||||
double searchAmount = 0,
|
||||
QDateTime searchTime = {},
|
||||
QString materialId = {},
|
||||
QObject *parent = nullptr
|
||||
);
|
||||
|
||||
@ -43,6 +44,7 @@ private:
|
||||
const int m_maxCount; // 0 = фильтр по 24ч, >0 = N последних
|
||||
const double m_searchAmount; // сумма в рублях (0 = не искать конкретную)
|
||||
const QDateTime m_searchTime; // примерное время (UTC)
|
||||
const QString m_materialId; // UUID задачи (для сохранения дампов)
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
#include <leptonica/allheaders.h>
|
||||
|
||||
#include "adb/AdbUtils.h"
|
||||
#include "dump/TaskDumpRecorder.h"
|
||||
#include "dao/MaterialDAO.h"
|
||||
#include "dao/BankProfileDAO.h"
|
||||
#include "dao/DeviceDAO.h"
|
||||
@ -90,6 +91,8 @@ void PayByCardScript::doStart() {
|
||||
}
|
||||
|
||||
const QString &adbId = device.adbSerial;
|
||||
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber};
|
||||
TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_CARD", adbId, dumpMeta, &m_error);
|
||||
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
|
||||
|
||||
@ -173,19 +176,27 @@ void PayByCardScript::makePayment(
|
||||
Node cardBtn;
|
||||
bool paymentsPageLoaded = false;
|
||||
bool titleSeen = false;
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
if (!checkApp()) return;
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||||
Node title = xmlScreenParser.findTextNode("Платежи и переводы");
|
||||
if (!title.isEmpty()) titleSeen = true;
|
||||
cardBtn = xmlScreenParser.findButtonNode("По номеру карты", false);
|
||||
if (!title.isEmpty() && !cardBtn.isEmpty()) {
|
||||
paymentsPageLoaded = true;
|
||||
break;
|
||||
{
|
||||
FreezeDetector freezeCheck;
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
if (!checkApp()) return;
|
||||
const QString xml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter);
|
||||
xmlScreenParser.parseAndSaveXml(xml);
|
||||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||||
if (freezeCheck.noticeAndCheckFrozen(xml)) {
|
||||
qWarning() << "[PayByCard] screen frozen waiting Платежи и переводы";
|
||||
break;
|
||||
}
|
||||
Node title = xmlScreenParser.findTextNode("Платежи и переводы");
|
||||
if (!title.isEmpty()) titleSeen = true;
|
||||
cardBtn = xmlScreenParser.findButtonNode("По номеру карты", false);
|
||||
if (!title.isEmpty() && !cardBtn.isEmpty()) {
|
||||
paymentsPageLoaded = true;
|
||||
break;
|
||||
}
|
||||
qDebug() << "[PayByCard] Waiting for 'Платежи и переводы' + 'По номеру карты', attempt" << attempt + 1 << "/ 10";
|
||||
QThread::msleep(1500);
|
||||
}
|
||||
qDebug() << "[PayByCard] Waiting for 'Платежи и переводы' + 'По номеру карты', attempt" << attempt + 1 << "/ 10";
|
||||
QThread::msleep(1500);
|
||||
}
|
||||
|
||||
if (!paymentsPageLoaded) {
|
||||
@ -203,17 +214,25 @@ void PayByCardScript::makePayment(
|
||||
|
||||
// 4. Ждём экран "Перевод по номеру карты"
|
||||
bool cardScreenLoaded = false;
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
if (!checkApp()) return;
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||||
Node title = xmlScreenParser.findTextNode("Перевод по номеру карты");
|
||||
if (!title.isEmpty()) {
|
||||
cardScreenLoaded = true;
|
||||
break;
|
||||
{
|
||||
FreezeDetector freezeCheck;
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
if (!checkApp()) return;
|
||||
const QString xml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter);
|
||||
xmlScreenParser.parseAndSaveXml(xml);
|
||||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||||
if (freezeCheck.noticeAndCheckFrozen(xml)) {
|
||||
qWarning() << "[PayByCard] screen frozen waiting 'Перевод по номеру карты'";
|
||||
break;
|
||||
}
|
||||
Node title = xmlScreenParser.findTextNode("Перевод по номеру карты");
|
||||
if (!title.isEmpty()) {
|
||||
cardScreenLoaded = true;
|
||||
break;
|
||||
}
|
||||
qDebug() << "[PayByCard] Waiting for 'Перевод по номеру карты', attempt" << attempt + 1 << "/ 10";
|
||||
QThread::msleep(1500);
|
||||
}
|
||||
qDebug() << "[PayByCard] Waiting for 'Перевод по номеру карты', attempt" << attempt + 1 << "/ 10";
|
||||
QThread::msleep(1500);
|
||||
}
|
||||
|
||||
if (!cardScreenLoaded) {
|
||||
@ -247,6 +266,11 @@ void PayByCardScript::makePayment(
|
||||
AdbUtils::inputText(deviceId, m_cardNumber);
|
||||
QThread::msleep(2000);
|
||||
|
||||
// Закрываем системную клавиатуру — она перекрывает поле суммы и кнопку
|
||||
// "Перевести" внизу, а на TECNO ещё мешает Ozon WebView дорендерить AT.
|
||||
AdbUtils::hideKeyboard(deviceId);
|
||||
QThread::msleep(800);
|
||||
|
||||
// 6. Ждём распознавания карты и вводим сумму
|
||||
Node sumInput;
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
@ -255,6 +279,35 @@ void PayByCardScript::makePayment(
|
||||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||||
sumInput = xmlScreenParser.findEditTextByHint("Сумма от");
|
||||
if (!sumInput.isEmpty()) break;
|
||||
|
||||
// Fallback для устройств, где Ozon WebView не отдаёт hint (TECNO).
|
||||
// На экране "Перевод по номеру карты" всегда 2 EditText: карта (сверху)
|
||||
// и сумма (снизу). Стратегии по приоритету:
|
||||
// 1) Svelte генерирует resource-id="input___xxxx" ТОЛЬКО для суммы —
|
||||
// берём этот EditText.
|
||||
// 2) Если rid нет (старая версия Ozon UI) — берём второй EditText
|
||||
// по y-порядку (карта y1=меньше, сумма y1=больше).
|
||||
QList<Node> candidateInputs;
|
||||
for (const Node &n : xmlScreenParser.findAllNodes()) {
|
||||
if (n.className != "android.widget.EditText") continue;
|
||||
if (n.height() <= 0 || n.height() > height * 4 / 5) continue;
|
||||
candidateInputs.append(n);
|
||||
}
|
||||
// Strategy 1: rid="input___"
|
||||
for (const Node &n : candidateInputs) {
|
||||
if (n.resourceId.startsWith("input___")) {
|
||||
sumInput = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Strategy 2: bottom-most по y1 (если их 2 — берём второй)
|
||||
if (sumInput.isEmpty() && candidateInputs.size() >= 2) {
|
||||
std::sort(candidateInputs.begin(), candidateInputs.end(),
|
||||
[](const Node &a, const Node &b) { return a.y1() < b.y1(); });
|
||||
sumInput = candidateInputs.last();
|
||||
}
|
||||
if (!sumInput.isEmpty()) break;
|
||||
|
||||
qDebug() << "[PayByCard] Waiting for 'Сумма от' input, attempt" << attempt + 1 << "/ 10";
|
||||
QThread::msleep(1500);
|
||||
}
|
||||
@ -349,10 +402,16 @@ void PayByCardScript::makePayment(
|
||||
}
|
||||
|
||||
// 9. Ждём результат: "Успешно" или "Не удалось"
|
||||
FreezeDetector resultFreeze;
|
||||
for (int attempt = 0; attempt < 15; ++attempt) {
|
||||
if (!checkApp()) return;
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||
const QString xml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter);
|
||||
xmlScreenParser.parseAndSaveXml(xml);
|
||||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||||
if (resultFreeze.noticeAndCheckFrozen(xml)) {
|
||||
qWarning() << "[PayByCard] screen frozen while waiting payment result";
|
||||
break;
|
||||
}
|
||||
|
||||
// Если PIN-код появился позже — вводим
|
||||
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||||
@ -382,48 +441,19 @@ void PayByCardScript::makePayment(
|
||||
const int txId = TransactionDAO::insertTransaction(tx);
|
||||
qDebug() << "[PayByCard] Saved transaction to DB, id:" << txId << "fee:" << fee;
|
||||
|
||||
// Ищем кнопку "Документы" и достаём ID операции
|
||||
// Скриншот экрана "Успешно" — отдаём как receipt-image и в EventHandler.
|
||||
// Раньше дополнительно тапали "Документы" + ждали "Поделиться" + OCR
|
||||
// ради bank_transaction_id, но это медленно (до 30 сек ожидания) и
|
||||
// нестабильно. Теперь полагаемся на fallback-ID.
|
||||
QString externalId;
|
||||
Node docBtn = xmlScreenParser.findButtonNode("Документы", false);
|
||||
if (!docBtn.isEmpty() && txId != -1) {
|
||||
qDebug() << "[PayByCard] 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() << "[PayByCard] Waiting for 'Поделиться', attempt" << da + 1 << "/ 20";
|
||||
QThread::msleep(1500);
|
||||
if (txId != -1) {
|
||||
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
|
||||
if (!screenshot.isEmpty()) {
|
||||
m_resultScreenshot = screenshot;
|
||||
emit screenshotReady(m_resultScreenshot);
|
||||
TransactionDAO::updateReceiptImage(txId, screenshot);
|
||||
qDebug() << "[PayByCard] Receipt saved to DB, txId:" << txId;
|
||||
}
|
||||
|
||||
if (docLoaded) {
|
||||
QThread::msleep(500);
|
||||
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
|
||||
if (!screenshot.isEmpty()) {
|
||||
m_resultScreenshot = screenshot;
|
||||
emit screenshotReady(m_resultScreenshot);
|
||||
TransactionDAO::updateReceiptImage(txId, screenshot);
|
||||
qDebug() << "[PayByCard] Receipt saved to DB, txId:" << txId;
|
||||
|
||||
const QString ocrText = recognizeTextFromImage(screenshot);
|
||||
if (!ocrText.isEmpty()) {
|
||||
qDebug() << "[PayByCard] OCR result:\n" << ocrText;
|
||||
externalId = extractTransactionId(ocrText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qWarning() << "[PayByCard] 'Поделиться' not found, skipping receipt";
|
||||
}
|
||||
|
||||
AdbUtils::goBack(deviceId);
|
||||
QThread::msleep(1000);
|
||||
}
|
||||
|
||||
if (externalId.isEmpty() && txId != -1) {
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
#include <leptonica/allheaders.h>
|
||||
|
||||
#include "adb/AdbUtils.h"
|
||||
#include "dump/TaskDumpRecorder.h"
|
||||
#include "dao/MaterialDAO.h"
|
||||
#include "dao/BankProfileDAO.h"
|
||||
#include "dao/DeviceDAO.h"
|
||||
@ -91,6 +92,8 @@ void PayByPhoneScript::doStart() {
|
||||
}
|
||||
|
||||
const QString &adbId = device.adbSerial;
|
||||
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}};
|
||||
TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error);
|
||||
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
|
||||
|
||||
@ -255,11 +258,18 @@ void PayByPhoneScript::makePayment(
|
||||
bool sumScreenLoaded = false;
|
||||
bool bankSelected = false;
|
||||
bool otherBankClicked = false;
|
||||
FreezeDetector freezeCheck;
|
||||
for (int attempt = 0; attempt < 20; ++attempt) {
|
||||
if (!checkApp()) return;
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||
const QString currentDumpXml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter);
|
||||
xmlScreenParser.parseAndSaveXml(currentDumpXml);
|
||||
closeGooglePlayBannerIfVisible(deviceId, width, height);
|
||||
|
||||
if (freezeCheck.noticeAndCheckFrozen(currentDumpXml)) {
|
||||
qWarning() << "[PayByPhone] 4 identical dumps in a row — screen frozen, aborting";
|
||||
break;
|
||||
}
|
||||
|
||||
// Проверяем: уже на экране суммы?
|
||||
Node sumTitle = xmlScreenParser.findTextNode("По номеру телефона");
|
||||
if (!sumTitle.isEmpty() && sumTitle.y() < height / 7) {
|
||||
@ -404,11 +414,29 @@ void PayByPhoneScript::makePayment(
|
||||
}
|
||||
|
||||
// 7. Вводим сумму
|
||||
// На экране "По номеру телефона" обычно 2 EditText: сумма (сверху) и
|
||||
// сообщение получателю (снизу). Оба имеют rid="input___xxxx" в Svelte UI.
|
||||
// Берём верхний (минимальный y1) — это amount.
|
||||
Node sumInput;
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
if (!checkApp()) return;
|
||||
QList<Node> candidateInputs;
|
||||
for (const Node &n : xmlScreenParser.findAllNodes()) {
|
||||
if (n.className != "android.widget.EditText") continue;
|
||||
if (n.height() <= 0 || n.height() > height * 4 / 5) continue;
|
||||
if (!n.resourceId.startsWith("input___")) continue;
|
||||
candidateInputs.append(n);
|
||||
}
|
||||
if (!candidateInputs.isEmpty()) {
|
||||
std::sort(candidateInputs.begin(), candidateInputs.end(),
|
||||
[](const Node &a, const Node &b) { return a.y1() < b.y1(); });
|
||||
sumInput = candidateInputs.first(); // верхний = amount
|
||||
break;
|
||||
}
|
||||
// Старая логика — если нет ни одного "input___": берём любой EditText.
|
||||
sumInput = xmlScreenParser.findFirstEditText();
|
||||
if (!sumInput.isEmpty()) break;
|
||||
|
||||
qDebug() << "[PayByPhone] Waiting for sum input field, attempt" << attempt + 1 << "/ 10";
|
||||
QThread::msleep(1500);
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
|
||||
@ -534,48 +562,19 @@ void PayByPhoneScript::makePayment(
|
||||
const int txId = TransactionDAO::insertTransaction(tx);
|
||||
qDebug() << "[PayByPhone] Saved transaction to DB, id:" << txId;
|
||||
|
||||
// Ищем кнопку "Документы" и достаём ID операции
|
||||
// Скриншот экрана "Успешно" — отдаём как receipt-image и в EventHandler.
|
||||
// Раньше дополнительно тапали "Документы" + ждали "Поделиться" + OCR
|
||||
// ради bank_transaction_id, но это медленно (до 30 сек ожидания) и
|
||||
// нестабильно. Теперь полагаемся на fallback-ID из phone+timestamp.
|
||||
QString externalId;
|
||||
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 (txId != -1) {
|
||||
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
|
||||
if (!screenshot.isEmpty()) {
|
||||
m_resultScreenshot = screenshot;
|
||||
emit screenshotReady(m_resultScreenshot);
|
||||
TransactionDAO::updateReceiptImage(txId, screenshot);
|
||||
qDebug() << "[PayByPhone] Receipt saved to DB, txId:" << txId;
|
||||
}
|
||||
|
||||
if (docLoaded) {
|
||||
QThread::msleep(500);
|
||||
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
|
||||
if (!screenshot.isEmpty()) {
|
||||
m_resultScreenshot = screenshot;
|
||||
emit screenshotReady(m_resultScreenshot);
|
||||
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;
|
||||
externalId = extractTransactionId(ocrText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qWarning() << "[PayByPhone] 'Поделиться' not found, skipping receipt";
|
||||
}
|
||||
|
||||
AdbUtils::goBack(deviceId);
|
||||
QThread::msleep(1000);
|
||||
}
|
||||
|
||||
if (externalId.isEmpty() && txId != -1) {
|
||||
|
||||
@ -1439,18 +1439,69 @@ bool ScreenXmlParser::isTransactionListScreen() {
|
||||
QList<Node> ScreenXmlParser::findTransactionButtons(const int maxCount) {
|
||||
const QChar rubleSign(0x20BD); // ₽
|
||||
QList<Node> buttons;
|
||||
|
||||
// В новом Svelte-UI Ozon ноды устроены так: Button-обёртка с пустым text,
|
||||
// а сам текст транзакции живёт во вложенной android.view.View с теми же
|
||||
// bounds. В старом UI текст был прямо в Button. Поддерживаем оба варианта:
|
||||
// сначала сматчим Button с непустым text, затем пройдёмся по пустым
|
||||
// Button-ам и подтянем текст из перекрывающейся View с ₽-знаком.
|
||||
auto isSummary = [](const QString &t) {
|
||||
return t.startsWith("Расходы") || t.startsWith("Доходы");
|
||||
};
|
||||
|
||||
for (const Node &node : m_nodes) {
|
||||
if (node.className != "android.widget.Button") continue;
|
||||
const QString text = node.text.trimmed();
|
||||
if (!text.contains(rubleSign)) continue;
|
||||
// Пропускаем сводки "Расходы", "Доходы"
|
||||
if (text.startsWith("Расходы") || text.startsWith("Доходы")) continue;
|
||||
buttons.append(node);
|
||||
if (text.isEmpty()) {
|
||||
// Ищем View с тем же bounds (или внутри Button-bounds), содержащую ₽.
|
||||
// У транзакционной строки высота ~170px — отсекаем мелкие декоры.
|
||||
if (node.height() < 80 || node.height() > 260) continue;
|
||||
QString viewText;
|
||||
for (const Node &v : m_nodes) {
|
||||
if (v.className != "android.view.View") continue;
|
||||
if (!v.text.contains(rubleSign)) continue;
|
||||
if (v.y1() < node.y1() || v.y2() > node.y2()) continue;
|
||||
if (v.x1() < node.x1() || v.x2() > node.x2()) continue;
|
||||
viewText = v.text.trimmed();
|
||||
break;
|
||||
}
|
||||
if (viewText.isEmpty() || isSummary(viewText)) continue;
|
||||
// Возвращаем Button с подставленным текстом, чтобы вызывающий код
|
||||
// (parseTransactionButtonText / makeTap по координатам) работал
|
||||
// прозрачно для обоих UI-вариантов.
|
||||
Node enriched = node;
|
||||
enriched.text = viewText;
|
||||
buttons.append(enriched);
|
||||
} else {
|
||||
if (!text.contains(rubleSign)) continue;
|
||||
if (isSummary(text)) continue;
|
||||
buttons.append(node);
|
||||
}
|
||||
if (buttons.size() >= maxCount) break;
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
|
||||
bool ScreenXmlParser::hasCollapsedTransactionsBelow(const int screenHeight) {
|
||||
const QChar rubleSign(0x20BD);
|
||||
// Off-screen строка в Ozon WebView выглядит так: bounds=[0,Y][W,Y] (height=0)
|
||||
// на нижнем краю viewport. Текст транзакции лежит во внутреннем
|
||||
// android.widget.Button (внешний wrapper-View имеет пустой text).
|
||||
// Ищем именно Button-ы с h=0 и ₽ в тексте — в этом случае и количество
|
||||
// совпадает с числом скрытых строк.
|
||||
const int bottomHalf = screenHeight / 2;
|
||||
for (const Node &node : m_nodes) {
|
||||
if (node.className != "android.widget.Button") continue;
|
||||
if (node.height() != 0) continue;
|
||||
if (node.y1() < bottomHalf) continue;
|
||||
const QString text = node.text.trimmed();
|
||||
if (text.isEmpty() || !text.contains(rubleSign)) continue;
|
||||
if (text.startsWith("Расходы") || text.startsWith("Доходы")) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
TransactionInfo ScreenXmlParser::parseTransactionButtonText(const QString &text) {
|
||||
const QChar rubleSign(0x20BD); // ₽
|
||||
const QChar unicodeMinus(0x2212); // −
|
||||
|
||||
@ -90,6 +90,13 @@ public:
|
||||
// Возвращает кнопки транзакций на экране "Операции" (до maxCount штук)
|
||||
QList<Node> findTransactionButtons(int maxCount);
|
||||
|
||||
// Ozon WebView рендерит tappable-Button только для видимых транзакций.
|
||||
// Off-screen элементы остаются в accessibility-tree как android.view.View
|
||||
// с "схлопнутыми" bounds (height == 0). Если такие View с ₽-знаком есть —
|
||||
// значит в списке осталось что прокручивать. Используется для детекции
|
||||
// того, что скролл не завершён.
|
||||
bool hasCollapsedTransactionsBelow(int screenHeight);
|
||||
|
||||
// Парсит имя и сумму из текста кнопки: "NAME + 150 ₽ CATEGORY"
|
||||
TransactionInfo parseTransactionButtonText(const QString &text);
|
||||
|
||||
|
||||
@ -478,7 +478,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
||||
const QDateTime searchTime = QDateTime::fromString(body["created_at"].toString(), Qt::ISODate);
|
||||
|
||||
if (appCode == "ozon") {
|
||||
auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime);
|
||||
auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime, account.materialId);
|
||||
setup(w); connectScreenshot(w);
|
||||
connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this,
|
||||
[this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; },
|
||||
@ -487,7 +487,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
||||
else if (appCode == "black")
|
||||
setup(new Black::GetLastTransactionsScript(adbSerial, appCode));
|
||||
else if (appCode == "dushanbe_city_bank") {
|
||||
auto *w = new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, searchAmount, searchTime);
|
||||
auto *w = new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, searchAmount, searchTime, account.materialId);
|
||||
setup(w); connectScreenshotDushanbe(w);
|
||||
connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this,
|
||||
[this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; },
|
||||
|
||||
@ -1,27 +1,7 @@
|
||||
{
|
||||
"bank_profiles": [
|
||||
{
|
||||
"id": "b8763ad9-50bd-4d8c-b6f1-cc122119e2df",
|
||||
"bank_name": "dushanbe_city_bank",
|
||||
"state": "enabled",
|
||||
"phone_number": "+992185555519",
|
||||
"first_name": "Мехрубон",
|
||||
"middle_name": null,
|
||||
"last_name": "Имомкулзода",
|
||||
"currency": 972,
|
||||
"device_id": "986cb259-dfec-4421-8735-4fb126cd1917",
|
||||
"device_label": "Pixel 7",
|
||||
"materials": [
|
||||
{
|
||||
"id": "36002ac2-6f4d-49da-8892-18db723a31c7",
|
||||
"state": "enabled",
|
||||
"card_number": "9762000207113258",
|
||||
"currency": 972
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ec6b5712-067c-4354-8641-a3f19a9fdef1",
|
||||
"id": "21aa26c8-ea89-45ea-a283-ee6d55ead82b",
|
||||
"bank_name": "ozon",
|
||||
"state": "enabled",
|
||||
"phone_number": "+79538038318",
|
||||
@ -33,13 +13,7 @@
|
||||
"device_label": "Pixel 7",
|
||||
"materials": [
|
||||
{
|
||||
"id": "50393cc1-57e4-4293-9a59-6cdd1d9ce6cb",
|
||||
"state": "enabled",
|
||||
"card_number": "2204321035207919",
|
||||
"currency": 643
|
||||
},
|
||||
{
|
||||
"id": "06a1ae15-abd2-4d90-aac0-77f4a4fd32e8",
|
||||
"id": "3a2ca76d-f735-4006-b521-66bccaf821a4",
|
||||
"state": "enabled",
|
||||
"card_number": "2204321086109865",
|
||||
"currency": 643
|
||||
@ -47,7 +21,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "a3fa08d3-58d7-4744-886c-c81afa707dcd",
|
||||
"id": "524c4cfe-9522-4434-90d1-9ee80aae92b2",
|
||||
"bank_name": "ozon",
|
||||
"state": "enabled",
|
||||
"phone_number": "+79855532896",
|
||||
@ -59,7 +33,7 @@
|
||||
"device_label": "TECNO KM4",
|
||||
"materials": [
|
||||
{
|
||||
"id": "7e3a7e4e-0d56-4d66-ac44-2b240ec33620",
|
||||
"id": "19d1a302-7155-4d68-b596-5c7fb7cf6c2e",
|
||||
"state": "enabled",
|
||||
"card_number": "2204320439130982",
|
||||
"currency": 643
|
||||
|
||||
@ -85,7 +85,7 @@ def query_active_materials(db_path: Path):
|
||||
FROM devices d
|
||||
JOIN bank_profiles bp ON bp.device_id = d.id
|
||||
JOIN materials m ON m.device_id = d.id AND m.app_code = bp.code
|
||||
WHERE bp.status = 'active' AND m.status = 'active'
|
||||
WHERE bp.pin_code_checked = 'yes' AND m.status = 'active'
|
||||
AND m.material_id != '' AND bp.bank_profile_id != ''
|
||||
ORDER BY d.name, bp.code
|
||||
""").fetchall()
|
||||
|
||||
@ -428,7 +428,15 @@ def task_result():
|
||||
# Unblock this material for the next task delivery
|
||||
_in_flight.discard(mid)
|
||||
|
||||
key = (mid, body.get("type"))
|
||||
# Клиент присылает ошибки с type="ERROR", но оригинальный тип задачи
|
||||
# лежит в data.event_type — им и матчимся, иначе ошибка повиснет
|
||||
# как unsolicited без привязки к задаче/устройству.
|
||||
rtype = body.get("type")
|
||||
if rtype == "ERROR":
|
||||
data = body.get("data") or {}
|
||||
rtype = data.get("event_type") or rtype
|
||||
|
||||
key = (mid, rtype)
|
||||
idx = None
|
||||
if _pending_by_key[key]:
|
||||
idx = _pending_by_key[key].popleft()
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"bank_name": "dushanbe_city_bank",
|
||||
"material_id": "36002ac2-6f4d-49da-8892-18db723a31c7",
|
||||
"type": "FETCH_PROFILE",
|
||||
"body": {}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"bank_name": "ozon",
|
||||
"material_id": "50393cc1-57e4-4293-9a59-6cdd1d9ce6cb",
|
||||
"type": "FETCH_PROFILE",
|
||||
"body": {}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"bank_name": "ozon",
|
||||
"material_id": "06a1ae15-abd2-4d90-aac0-77f4a4fd32e8",
|
||||
"material_id": "3a2ca76d-f735-4006-b521-66bccaf821a4",
|
||||
"type": "FETCH_PROFILE",
|
||||
"body": {}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"bank_name": "ozon",
|
||||
"material_id": "7e3a7e4e-0d56-4d66-ac44-2b240ec33620",
|
||||
"material_id": "19d1a302-7155-4d68-b596-5c7fb7cf6c2e",
|
||||
"type": "FETCH_PROFILE",
|
||||
"body": {}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user