diff --git a/android/black/GetCardInfoScript.cpp b/android/black/GetCardInfoScript.cpp index 237b25b..dca0684 100644 --- a/android/black/GetCardInfoScript.cpp +++ b/android/black/GetCardInfoScript.cpp @@ -37,7 +37,7 @@ void GetCardInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("black", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); // Читаем текущий экран diff --git a/android/black/GetProfileInfoScript.cpp b/android/black/GetProfileInfoScript.cpp index 18abbf0..6fa2a8d 100644 --- a/android/black/GetProfileInfoScript.cpp +++ b/android/black/GetProfileInfoScript.cpp @@ -35,7 +35,7 @@ void GetProfileInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); // Читаем текущий экран diff --git a/android/black/LoginAndCheckAccountsScript.cpp b/android/black/LoginAndCheckAccountsScript.cpp index 11e6579..63e42ba 100644 --- a/android/black/LoginAndCheckAccountsScript.cpp +++ b/android/black/LoginAndCheckAccountsScript.cpp @@ -36,7 +36,7 @@ void LoginAndCheckAccountsScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/android/dump/TaskDumpRecorder.cpp b/android/dump/TaskDumpRecorder.cpp index 531d081..07aefe2 100644 --- a/android/dump/TaskDumpRecorder.cpp +++ b/android/dump/TaskDumpRecorder.cpp @@ -180,6 +180,8 @@ QString TaskDumpRecorder::buildCaption(const QString &errorMessage) const { if (!desktopId.isEmpty()) body += QStringLiteral("Desktop: %1\n").arg(desktopId.toHtmlEscaped()); body += QStringLiteral("Device: %1").arg(m_adbDeviceIdRaw.toHtmlEscaped()); + if (!m_meta.androidId.isEmpty()) + body += QStringLiteral("\nAndroidId: %1").arg(m_meta.androidId.toHtmlEscaped()); if (!m_meta.bankProfileId.isEmpty()) body += QStringLiteral("\nProfile: %1").arg(m_meta.bankProfileId.toHtmlEscaped()); if (!m_meta.materialId.isEmpty()) @@ -251,6 +253,7 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) { ts << "bank: " << m_bank << "\n" << "task: " << m_taskKind << "\n" << "device: " << m_adbDeviceIdRaw << "\n"; + if (!m_meta.androidId.isEmpty()) ts << "android_id: " << m_meta.androidId << "\n"; if (!m_meta.bankProfileId.isEmpty()) ts << "profile_id: " << m_meta.bankProfileId << "\n"; if (!m_meta.profilePhone.isEmpty()) ts << "profile_phone: " << m_meta.profilePhone << "\n"; if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n"; diff --git a/android/dump/TaskDumpRecorder.h b/android/dump/TaskDumpRecorder.h index fb8ae89..6e2ccea 100644 --- a/android/dump/TaskDumpRecorder.h +++ b/android/dump/TaskDumpRecorder.h @@ -13,6 +13,7 @@ struct TaskDumpMeta { QString card; // пусто = нет QString bankProfileId; // серверный UUID банковского профиля (детект в общих логах) QString profilePhone; // телефон банковского профиля (не получателя) + QString androidId; // стабильный android_id устройства (в отличие от adbSerial не меняется) }; class TaskDumpRecorder { diff --git a/android/dushanbe/GetCardInfoScript.cpp b/android/dushanbe/GetCardInfoScript.cpp index 1f891d9..40c7772 100644 --- a/android/dushanbe/GetCardInfoScript.cpp +++ b/android/dushanbe/GetCardInfoScript.cpp @@ -36,7 +36,7 @@ void GetCardInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp index f921ab9..566e3a4 100644 --- a/android/dushanbe/GetLastTransactionsScript.cpp +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -18,6 +18,8 @@ #include +#include + // Душанбе показывает суммы как "100,50", "100.50 TJS", "1 000,50", // "1 000.50 TJS" и т.п. Голый QString::toDouble на таком вернёт 0, // потому что требует, чтобы вся строка была валидным числом. @@ -276,7 +278,7 @@ void GetLastTransactionsScript::doStart() { } const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first; - const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); @@ -386,16 +388,31 @@ void GetLastTransactionsScript::doStart() { qWarning() << "[Dushanbe::GetLastTransactions] No account found"; } + // История отсортирована сверху-вниз: новые операции выше старых. Ищем цели + // в том же порядке (по времени, от новых к старым) — тогда один непрерывный + // проход вниз находит все, и возвращаться к началу между целями не нужно. + QList> targets = m_targets; + std::sort(targets.begin(), targets.end(), + [](const QPair &a, const QPair &b) { + return a.second > b.second; // по убыванию времени (новые первыми) + }); + // Ищем каждую цель из списка. Найденные отдаём сразу через transactionParsed; // ненайденные копим и репортим одной агрегированной ошибкой в конце. QStringList notFound; - for (int i = 0; i < m_targets.size(); ++i) { + for (int i = 0; i < targets.size(); ++i) { if (QThread::currentThread()->isInterruptionRequested()) { m_error = "Interrupted"; break; } - const double amount = m_targets[i].first; - const QDateTime time = m_targets[i].second; - // Перед каждой целью (кроме первой) возвращаемся к началу Выписки. - if (i > 0) scrollHistoryToTop(device); - if (!searchOne(device, accountId, amount, time)) { + const double amount = targets[i].first; + const QDateTime time = targets[i].second; + // Проход идёт вниз от текущей позиции. Если цель не нашлась — возможно, + // предыдущая (более новая) цель отсутствовала и увела список вниз, а эта + // лежит выше. Тогда надёжно возвращаемся к началу Выписки и пробуем ещё раз. + bool found = searchOne(device, accountId, amount, time); + if (!found && m_error != "Interrupted") { + scrollHistoryToTop(device); + found = searchOne(device, accountId, amount, time); + } + if (!found) { if (m_error == "Interrupted") break; notFound << QString("amount=%1 time=%2") .arg(QString::number(amount, 'f', 2), time.toString(Qt::ISODate)); @@ -423,16 +440,48 @@ void GetLastTransactionsScript::doStart() { } void GetLastTransactionsScript::scrollHistoryToTop(const DeviceInfo &device) { - // Свайпы сверху-вниз поднимают список Выписки к началу. - for (int i = 0; i < 4; ++i) { + // Свайпы сверху-вниз поднимают список Выписки к началу. Поиск ОДНОЙ цели + // (searchOne) мог проскроллить вниз до maxScrolls(=10) раз, поэтому + // фиксированного числа свайпов вверх не хватает — крутим, пока верх списка + // не перестанет меняться (значит достигли начала), с запасом по итерациям. + static const QRegularExpression reTime(R"(\d{2}:\d{2}:\d{2})"); + + // Сигнатура верха списка: content-desc самой верхней View-строки операции + // (last line — время HH:MM:SS). Пустая строка — список не распознан/пуст. + auto topSignature = [&]() -> QString { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + QString best; + int bestY = device.screenHeight + 1; + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (n.className != "android.view.View") continue; + const QStringList lines = n.contentDesc.split('\n'); + if (lines.size() < 5) continue; + if (!reTime.match(lines.last().trimmed()).hasMatch()) continue; + if (n.y1() < bestY) { bestY = n.y1(); best = n.contentDesc; } + } + return best; + }; + + const int x = device.screenWidth / 2; + const int fromY = device.screenHeight / 3; // ниже шапки — без pull-to-refresh + const int toY = device.screenHeight * 3 / 4; // длинный бросок вниз → контент вверх + const int maxUpSwipes = 14; // > maxScrolls(10) с запасом + + QString prevTop = topSignature(); + int stable = 0; + for (int i = 0; i < maxUpSwipes; ++i) { if (QThread::currentThread()->isInterruptionRequested()) return; - AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, - device.screenHeight / 3, - device.screenWidth / 2, - device.screenHeight * 2 / 3, 300); - QThread::msleep(500); + AdbUtils::makeSwipe(m_deviceId, x, fromY, x, toY, 500); + QThread::msleep(700); + const QString curTop = topSignature(); + if (!curTop.isEmpty() && curTop == prevTop) { + if (++stable >= 2) break; // верх не меняется два свайпа подряд → начало списка + } else { + stable = 0; + prevTop = curTop; + } } - QThread::msleep(800); + QThread::msleep(500); } bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountId, diff --git a/android/dushanbe/GetProfileInfoScript.cpp b/android/dushanbe/GetProfileInfoScript.cpp index c5e924e..0c916cd 100644 --- a/android/dushanbe/GetProfileInfoScript.cpp +++ b/android/dushanbe/GetProfileInfoScript.cpp @@ -39,7 +39,7 @@ void GetProfileInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/android/dushanbe/LoginAndCheckAccountsScript.cpp b/android/dushanbe/LoginAndCheckAccountsScript.cpp index 2dcca1c..b4283e3 100644 --- a/android/dushanbe/LoginAndCheckAccountsScript.cpp +++ b/android/dushanbe/LoginAndCheckAccountsScript.cpp @@ -41,7 +41,7 @@ void LoginAndCheckAccountsScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/android/dushanbe/PayByCardScript.cpp b/android/dushanbe/PayByCardScript.cpp index e4fe131..70925b4 100644 --- a/android/dushanbe/PayByCardScript.cpp +++ b/android/dushanbe/PayByCardScript.cpp @@ -123,7 +123,7 @@ void PayByCardScript::doStart() { return; } - const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); diff --git a/android/dushanbe/PayByPhoneScript.cpp b/android/dushanbe/PayByPhoneScript.cpp index e56d186..4534c8c 100644 --- a/android/dushanbe/PayByPhoneScript.cpp +++ b/android/dushanbe/PayByPhoneScript.cpp @@ -120,7 +120,7 @@ void PayByPhoneScript::doStart() { return; } - const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_PHONE", device.adbSerial, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp index 6471570..129f77f 100644 --- a/android/ozon/GetCardInfoScript.cpp +++ b/android/ozon/GetCardInfoScript.cpp @@ -42,7 +42,7 @@ void GetCardInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); // Читаем текущий экран diff --git a/android/ozon/GetLastTransactionsScript.cpp b/android/ozon/GetLastTransactionsScript.cpp index ae30a74..4c71177 100644 --- a/android/ozon/GetLastTransactionsScript.cpp +++ b/android/ozon/GetLastTransactionsScript.cpp @@ -109,7 +109,7 @@ void GetLastTransactionsScript::doStart() { } const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first; - const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error); if (!navigateToTransactionList(device, app)) { diff --git a/android/ozon/GetProfileInfoScript.cpp b/android/ozon/GetProfileInfoScript.cpp index 207bd74..8a3d12e 100644 --- a/android/ozon/GetProfileInfoScript.cpp +++ b/android/ozon/GetProfileInfoScript.cpp @@ -37,7 +37,7 @@ void GetProfileInfoScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); // Читаем текущий экран diff --git a/android/ozon/LoginAndCheckAccountsScript.cpp b/android/ozon/LoginAndCheckAccountsScript.cpp index 615cf2f..0c25617 100644 --- a/android/ozon/LoginAndCheckAccountsScript.cpp +++ b/android/ozon/LoginAndCheckAccountsScript.cpp @@ -40,7 +40,7 @@ void LoginAndCheckAccountsScript::doStart() { return; } - const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); diff --git a/android/ozon/PayByCardScript.cpp b/android/ozon/PayByCardScript.cpp index 1f58039..0fd172a 100644 --- a/android/ozon/PayByCardScript.cpp +++ b/android/ozon/PayByCardScript.cpp @@ -91,7 +91,7 @@ void PayByCardScript::doStart() { } const QString &adbId = device.adbSerial; - const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_CARD", adbId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); diff --git a/android/ozon/PayByPhoneScript.cpp b/android/ozon/PayByPhoneScript.cpp index 52df1be..eb56176 100644 --- a/android/ozon/PayByPhoneScript.cpp +++ b/android/ozon/PayByPhoneScript.cpp @@ -100,7 +100,7 @@ void PayByPhoneScript::doStart() { } const QString &adbId = device.adbSerial; - const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone}; + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id}; TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index d8e5f01..c8751cc 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -348,11 +348,12 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event, // ещё не отправлено: при частичном успехе (часть списка найдена и уже // улетела на сервер) ретрай бессмыслен — переотправит дубли, поэтому // ненайденные просто репортим ошибкой ниже. - if (!result.isEmpty() && event.type == EventType::FetchTransactions - && m_parsedTxCount.value(key, 0) == 0 - && tryRetry(key, event, result)) { - return; - } + // Ретраи отключены: при ошибке сразу репортим как Error без повторов. + // if (!result.isEmpty() && event.type == EventType::FetchTransactions + // && m_parsedTxCount.value(key, 0) == 0 + // && tryRetry(key, event, result)) { + // return; + // } const EventStatus newStatus = result.isEmpty() ? EventStatus::Complete : EventStatus::Error; @@ -757,11 +758,12 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn // Поиск транзакции: таймаут тоже ретраим (ещё до 2 раз), но только если // ещё ни одной транзакции не отправлено (иначе ретрай переотправит дубли). - if (event.type == EventType::FetchTransactions - && m_parsedTxCount.value(key, 0) == 0 - && tryRetry(key, event, "Script timeout (5 min)")) { - return; - } + // Ретраи отключены: при таймауте сразу репортим как Error без повторов. + // if (event.type == EventType::FetchTransactions + // && m_parsedTxCount.value(key, 0) == 0 + // && tryRetry(key, event, "Script timeout (5 min)")) { + // return; + // } // Помечаем событие как ошибку EventDAO::updateEvent(event.id, EventStatus::Error, "Script timeout (5 min)"); diff --git a/views/widget/loader/DesktopSelectionWindow.cpp b/views/widget/loader/DesktopSelectionWindow.cpp index 2a19ce4..a017e40 100644 --- a/views/widget/loader/DesktopSelectionWindow.cpp +++ b/views/widget/loader/DesktopSelectionWindow.cpp @@ -40,7 +40,14 @@ DesktopSelectionWindow::DesktopSelectionWindow(const QJsonArray &desktops, QWidg const QStringList devices = deviceNamesOf(d); QString text = title; - if (!devices.isEmpty()) text += "\n" + devices.join("\n"); + if (!devices.isEmpty()) { + // При длинном списке показываем только первые 3 устройства, + // а в следующей строке — сколько ещё осталось. + const int kMaxShown = 3; + text += "\n" + devices.mid(0, kMaxShown).join("\n"); + if (devices.size() > kMaxShown) + text += QString("\n… ещё %1").arg(devices.size() - kMaxShown); + } auto *btn = new QPushButton(text); btn->setStyleSheet("text-align: left; padding: 8px;");