1
0
forked from BRT/arc

Refine transaction ID generation by replacing history-based retrieval with timestamp-based fallback logic. Adjust source card selection and transaction grouping in payment scripts.

This commit is contained in:
slava 2026-04-10 18:46:33 +07:00
parent fd99281dfb
commit ffb206303e
3 changed files with 129 additions and 122 deletions

View File

@ -294,10 +294,6 @@ void GetLastTransactionsScript::doStart() {
AdbUtils::makeTap(m_deviceId, okBtn.x(), okBtn.y()); AdbUtils::makeTap(m_deviceId, okBtn.x(), okBtn.y());
QThread::msleep(1000); QThread::msleep(1000);
} }
// Свайп вниз для обновления (pull-to-refresh)
const int x = device.screenWidth / 2;
AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight / 4, x, device.screenHeight * 3 / 4);
QThread::msleep(3000);
continue; continue;
} }
@ -339,11 +335,8 @@ void GetLastTransactionsScript::doStart() {
qDebug() << "[Dushanbe::GetLastTransactions] Error dialog on Выписка, tapping OK"; qDebug() << "[Dushanbe::GetLastTransactions] Error dialog on Выписка, tapping OK";
AdbUtils::makeTap(m_deviceId, okBtn.x(), okBtn.y()); AdbUtils::makeTap(m_deviceId, okBtn.x(), okBtn.y());
QThread::msleep(1000); QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
} }
const int x = device.screenWidth / 2;
AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight / 4, x, device.screenHeight * 3 / 4);
QThread::msleep(3000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
} }
// Находим accountId // Находим accountId
@ -364,43 +357,93 @@ void GetLastTransactionsScript::doStart() {
if (!tjTz.isValid()) tjTz = QTimeZone("Asia/Dushanbe"); if (!tjTz.isValid()) tjTz = QTimeZone("Asia/Dushanbe");
const int maxScrolls = 10; const int maxScrolls = 10;
struct VypiskaItem {
double amount = 0.0;
QString time; // "21:31:21"
QString date; // "05.04.2026"
Node node;
};
for (int scroll = 0; scroll <= maxScrolls; ++scroll) { for (int scroll = 0; scroll <= maxScrolls; ++scroll) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
const QList<ScreenXmlParser::TransactionItem> items = xmlScreenParser.parseTransactionItems();
// 1. Собираем список дат-групп из родительского ImageView "Обновлено: ...\n05.04.2026\n..."
// Поддерживаем "DD.MM.YYYY", "Сегодня", "Вчера"
const QDate todayLocal = QDateTime::currentDateTimeUtc().toTimeZone(tjTz).date();
QStringList groupDates;
static const QRegularExpression reDate(R"(^\d{2}\.\d{2}\.\d{4}$)");
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (n.className != "android.widget.ImageView") continue;
if (!n.contentDesc.startsWith(QString::fromUtf8("Обновлено"))) continue;
for (const QString &line : n.contentDesc.split('\n')) {
const QString t = line.trimmed();
if (reDate.match(t).hasMatch()) {
groupDates.append(t);
} else if (t.compare(QString::fromUtf8("Сегодня"), Qt::CaseInsensitive) == 0) {
groupDates.append(todayLocal.toString("dd.MM.yyyy"));
} else if (t.compare(QString::fromUtf8("Вчера"), Qt::CaseInsensitive) == 0) {
groupDates.append(todayLocal.addDays(-1).toString("dd.MM.yyyy"));
}
}
break;
}
// 2. Собираем View-транзакции (формат Выписки)
static const QRegularExpression reTime(R"(\d{2}:\d{2}:\d{2})");
QList<VypiskaItem> items;
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (n.className != "android.view.View") continue;
if (!n.contentDesc.contains("DCWallet") && !n.contentDesc.contains("***")) continue;
const QStringList lines = n.contentDesc.split('\n');
if (lines.size() < 5) continue;
const QString last = lines.last().trimmed();
if (!reTime.match(last).hasMatch()) continue;
VypiskaItem it;
bool ok = false;
it.amount = lines[2].trimmed().toDouble(&ok);
if (!ok) continue;
it.time = last;
it.node = n;
items.append(it);
}
// 3. Сортируем по Y и группируем по разрывам (> 50px → новая группа-день)
std::sort(items.begin(), items.end(), [](const VypiskaItem &a, const VypiskaItem &b) {
return a.node.y1() < b.node.y1();
});
int groupIdx = 0;
int prevY2 = -1;
for (VypiskaItem &it : items) {
if (prevY2 >= 0 && it.node.y1() - prevY2 > 50) ++groupIdx;
if (groupIdx < groupDates.size()) it.date = groupDates[groupIdx];
prevY2 = it.node.y2();
}
qDebug() << "[Dushanbe::GetLastTransactions] Vypiska items:" << items.size()
<< "groups:" << groupDates;
for (const auto &tx : items) { for (const auto &tx : items) {
if (qAbs(tx.amount - m_searchAmount) > 0.01) continue; if (qAbs(tx.amount - m_searchAmount) > 0.01) continue;
qDebug() << "[Dushanbe::GetLastTransactions] Amount match:" << tx.amount qDebug() << "[Dushanbe::GetLastTransactions] Amount match:" << tx.amount
<< "phone:" << tx.phone << "time:" << tx.time; << "date:" << tx.date << "time:" << tx.time;
// Предварительная проверка времени из списка (±1 час) // Проверка даты+времени (±1 час)
if (m_searchTime.isValid() && !tx.time.isEmpty()) { if (m_searchTime.isValid() && !tx.date.isEmpty() && !tx.time.isEmpty()) {
const QTime listTime = QTime::fromString(tx.time, "HH:mm:ss"); QDateTime txDt = QDateTime::fromString(tx.date + " " + tx.time,
const QTime searchLocal = m_searchTime.toTimeZone(tjTz).time(); "dd.MM.yyyy HH:mm:ss");
if (listTime.isValid() && searchLocal.isValid()) { if (txDt.isValid()) {
const int diffSecs = qAbs(listTime.secsTo(searchLocal)); txDt.setTimeZone(tjTz);
if (diffSecs > 3600 && diffSecs < 82800) { // 82800 = 24h - 1h (для полуночи) const qint64 diffSecs = qAbs(txDt.toUTC().secsTo(m_searchTime.toUTC()));
qDebug() << "[Dushanbe::GetLastTransactions] Time pre-filter skip:" if (diffSecs > 3600) {
<< tx.time << "vs" << searchLocal.toString("HH:mm:ss") qDebug() << "[Dushanbe::GetLastTransactions] Time diff too big:" << diffSecs << "sec, skipping";
<< "diff=" << diffSecs << "sec";
continue; continue;
} }
} }
} }
// Ищем соответствующий node чтобы по нему тапнуть Node txNode = tx.node;
Node txNode;
for (const Node &node : xmlScreenParser.findAllNodes()) {
if (node.className != "android.widget.ImageView") continue;
if (!node.contentDesc.contains("***")) continue;
// Проверяем что содержит нужную сумму и время
const QString amountStr = QString::number(tx.amount, 'f', 2);
if (node.contentDesc.contains(amountStr) && node.contentDesc.contains(tx.time)) {
txNode = node;
break;
}
}
if (txNode.x() == 0 && txNode.y() == 0) { if (txNode.x() == 0 && txNode.y() == 0) {
qWarning() << "[Dushanbe::GetLastTransactions] Cannot find node for transaction"; qWarning() << "[Dushanbe::GetLastTransactions] Cannot find node for transaction";

View File

@ -250,10 +250,31 @@ void PayByCardScript::makePayment(
AdbUtils::inputText(deviceId, amountStr); AdbUtils::inputText(deviceId, amountStr);
QThread::msleep(1000); QThread::msleep(1000);
// 7. Скрываем клавиатуру и нажимаем "Далее" // 6a. Скрываем клавиатуру и выбираем карту-источник (под полем "Комментарий")
AdbUtils::goBack(deviceId); AdbUtils::goBack(deviceId);
QThread::msleep(500); QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
{
const QString cardMask = "****" + m_account.lastNumbers;
Node cardNode;
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (!n.clickable) continue;
if (n.contentDesc.contains(cardMask)) { cardNode = n; break; }
}
if (cardNode.isEmpty()) {
m_error = "Source card not found: " + cardMask;
qWarning() << "[Dushanbe::PayByCard]" << m_error;
AppLogger::log("dushanbe/PayByCard", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
qDebug() << "[Dushanbe::PayByCard] Selecting source card:" << cardMask;
AdbUtils::makeTap(deviceId, cardNode.x(), cardNode.y());
QThread::msleep(500);
}
// 7. Нажимаем "Далее"
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее")); Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее"));
if (daleeBtn.isEmpty()) { if (daleeBtn.isEmpty()) {
daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false); daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false);
@ -409,53 +430,11 @@ void PayByCardScript::makePayment(
qDebug() << "[Dushanbe::PayByCard] Result: date=" << bankTime << "fee=" << fee; qDebug() << "[Dushanbe::PayByCard] Result: date=" << bankTime << "fee=" << fee;
} }
// Переходим в историю для получения ID операции через OCR // Генерируем ID транзакции из даты+времени (без перехода в историю)
// 1. Тапаем "На главную" (внизу экрана результата) const QString operationId = bankTime.isEmpty()
Node naGlavnuyuNode = xmlScreenParser.findNodeByContentDesc( ? QString("dushanbe_") + QDateTime::currentDateTimeUtc().toString("dd.MM.yyyy_HH:mm:ss")
QString::fromUtf8("Платеж выполнен"), true); : QString("dushanbe_") + QString(bankTime).replace(' ', '_');
if (!naGlavnuyuNode.isEmpty()) { qDebug() << "[Dushanbe::PayByCard] Generated operationId:" << operationId;
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; TransactionInfo tx;

View File

@ -250,10 +250,31 @@ void PayByPhoneScript::makePayment(
AdbUtils::inputText(deviceId, amountStr); AdbUtils::inputText(deviceId, amountStr);
QThread::msleep(1000); QThread::msleep(1000);
// 7. Скрываем клавиатуру и нажимаем "Далее" // 6a. Скрываем клавиатуру и выбираем карту-источник (под полем "Комментарий")
AdbUtils::goBack(deviceId); AdbUtils::goBack(deviceId);
QThread::msleep(500); QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
{
const QString cardMask = "****" + m_account.lastNumbers;
Node cardNode;
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (!n.clickable) continue;
if (n.contentDesc.contains(cardMask)) { cardNode = n; break; }
}
if (cardNode.isEmpty()) {
m_error = "Source card not found: " + cardMask;
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
qDebug() << "[Dushanbe::PayByPhone] Selecting source card:" << cardMask;
AdbUtils::makeTap(deviceId, cardNode.x(), cardNode.y());
QThread::msleep(500);
}
// 7. Нажимаем "Далее"
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее")); Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее"));
if (daleeBtn.isEmpty()) { if (daleeBtn.isEmpty()) {
daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false); daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false);
@ -400,47 +421,11 @@ void PayByPhoneScript::makePayment(
qDebug() << "[Dushanbe::PayByPhone] Result: date=" << bankTime << "fee=" << fee; qDebug() << "[Dushanbe::PayByPhone] Result: date=" << bankTime << "fee=" << fee;
} }
// Переходим в историю для получения ID операции через OCR // Генерируем ID транзакции из даты+времени (без перехода в историю)
Node naGlavnuyuNode = xmlScreenParser.findNodeByContentDesc( const QString operationId = bankTime.isEmpty()
QString::fromUtf8("Платеж выполнен"), true); ? QString("dushanbe_") + QDateTime::currentDateTimeUtc().toString("dd.MM.yyyy_HH:mm:ss")
if (!naGlavnuyuNode.isEmpty()) { : QString("dushanbe_") + QString(bankTime).replace(' ', '_');
const int tapY = height * 92 / 100; qDebug() << "[Dushanbe::PayByPhone] Generated operationId:" << operationId;
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; TransactionInfo tx;