1
0
forked from BRT/arc

Add account number parsing and database integration:

- Extend `ScreenXmlParser` with methods for detecting and extracting account numbers from multiple screens.
- Update database schema and DAO to support `account_number` field.
- Modify card information scripts to collect, save, and utilize account numbers.
- Enhance `GetCardInfoScript` logic to fetch account numbers via screen parsing and interaction.
This commit is contained in:
slava 2026-05-08 20:29:00 +07:00
parent 9a7a718d52
commit 377844e94b
10 changed files with 351 additions and 32 deletions

View File

@ -157,11 +157,84 @@ void GetCardInfoScript::doStart() {
continue; continue;
} }
// Достаём номер счёта: Настройки карты → Реквизиты карты → таб "Реквизиты счета".
QString accountNumber;
const Node settingsBtn = xmlScreenParser.findCardSettingsButton();
if (settingsBtn.x() > 0) {
AdbUtils::makeTap(m_deviceId, settingsBtn.x(), settingsBtn.y());
QThread::msleep(1500);
bool sheetFound = false;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isCardSettingsBottomSheet()) {
sheetFound = true;
break;
}
QThread::msleep(1000);
}
if (sheetFound) {
const Node requisitesItem = xmlScreenParser.findCardRequisitesMenuItem();
if (requisitesItem.x() > 0) {
AdbUtils::makeTap(m_deviceId, requisitesItem.x(), requisitesItem.y());
QThread::msleep(1500);
bool reqScreenFound = false;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isCardRequisitesScreen()) {
reqScreenFound = true;
break;
}
QThread::msleep(1000);
}
if (reqScreenFound) {
const Node accountTab = xmlScreenParser.findAccountRequisitesTab();
if (accountTab.x() > 0) {
AdbUtils::makeTap(m_deviceId, accountTab.x(), accountTab.y());
QThread::msleep(1500);
// Несколько попыток — данные могут подгружаться
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
accountNumber = xmlScreenParser.parseAccountNumber();
if (!accountNumber.isEmpty()) break;
QThread::msleep(1000);
}
qDebug() << "[Dushanbe::GetCardInfo] accountNumber for card" << i
<< ":" << accountNumber;
} else {
qWarning() << "[Dushanbe::GetCardInfo] Account tab not found for card" << i;
}
} else {
qWarning() << "[Dushanbe::GetCardInfo] Requisites screen not detected for card" << i;
}
} else {
qWarning() << "[Dushanbe::GetCardInfo] 'Реквизиты карты' menu item not found for card" << i;
}
} else {
qWarning() << "[Dushanbe::GetCardInfo] Card settings bottom sheet not detected for card" << i;
}
// Возврат на экран детали карты — bottom sheet и/или экран Реквизитов
for (int attempt = 0; attempt < 4; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isCardDetailScreen()) break;
AdbUtils::goBack(m_deviceId);
QThread::msleep(1200);
}
} else {
qWarning() << "[Dushanbe::GetCardInfo] 'Настройки карты' button not found for card" << i;
}
MaterialInfo account; MaterialInfo account;
account.deviceId = device.id; account.deviceId = device.id;
account.appCode = m_appCode; account.appCode = m_appCode;
account.lastNumbers = lastNumbers; account.lastNumbers = lastNumbers;
account.cardNumber = QString(fullNumber).remove(' '); account.cardNumber = QString(fullNumber).remove(' ');
account.accountNumber = accountNumber;
account.amount = cardBalance; account.amount = cardBalance;
account.currency = "TJS"; account.currency = "TJS";
account.description = cards[i].description.isEmpty() ? holder : cards[i].description; account.description = cards[i].description.isEmpty() ? holder : cards[i].description;
@ -187,6 +260,7 @@ void GetCardInfoScript::doStart() {
QJsonObject materialBody; QJsonObject materialBody;
materialBody["bank_profile_id"] = updatedApp.bankProfileId; materialBody["bank_profile_id"] = updatedApp.bankProfileId;
materialBody["card_number"] = QString(fullNumber).remove(' '); materialBody["card_number"] = QString(fullNumber).remove(' ');
materialBody["account_number"] = accountNumber;
materialBody["name"] = updatedApp.fullName; materialBody["name"] = updatedApp.fullName;
materialBody["state"] = materialStatusToString(savedAccount.status) == "active" ? "enabled" : "disabled"; materialBody["state"] = materialStatusToString(savedAccount.status) == "active" ? "enabled" : "disabled";
materialBody["currency"] = currencyCodes.value(savedAccount.currency.toUpper(), 972); materialBody["currency"] = currencyCodes.value(savedAccount.currency.toUpper(), 972);

View File

@ -410,6 +410,86 @@ QString ScreenXmlParser::parseCardDetailHolder() {
return {}; return {};
} }
Node ScreenXmlParser::findCardSettingsButton() {
for (const Node &node : m_nodes) {
if (node.clickable && node.contentDesc == QString::fromUtf8("Настройки карты")) {
return node;
}
}
return {};
}
bool ScreenXmlParser::isCardSettingsBottomSheet() {
bool hasRequisites = false;
bool hasBlock = false;
for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Реквизиты карты")) hasRequisites = true;
if (node.contentDesc == QString::fromUtf8("Заблокировать карту")) hasBlock = true;
if (hasRequisites && hasBlock) return true;
}
return false;
}
Node ScreenXmlParser::findCardRequisitesMenuItem() {
// На bottom sheet это ImageView; на экране "Реквизиты и информация"
// одноимённый таб — Button. Берём именно ImageView, чтобы не перепутать.
for (const Node &node : m_nodes) {
if (node.clickable
&& node.className == "android.widget.ImageView"
&& node.contentDesc == QString::fromUtf8("Реквизиты карты")) {
return node;
}
}
return {};
}
bool ScreenXmlParser::isCardRequisitesScreen() {
bool hasCardTab = false;
bool hasAccountTab = false;
for (const Node &node : m_nodes) {
if (node.className != "android.widget.Button") continue;
if (node.contentDesc == QString::fromUtf8("Реквизиты карты")) hasCardTab = true;
if (node.contentDesc == QString::fromUtf8("Реквизиты счета")) hasAccountTab = true;
if (hasCardTab && hasAccountTab) return true;
}
return false;
}
Node ScreenXmlParser::findAccountRequisitesTab() {
for (const Node &node : m_nodes) {
if (node.clickable
&& node.className == "android.widget.Button"
&& node.contentDesc == QString::fromUtf8("Реквизиты счета")) {
return node;
}
}
return {};
}
QString ScreenXmlParser::parseAccountNumber() {
// Берём ноду "Номер счета" (label) и ищем ближайший узел снизу
// с чисто цифровым content-desc — это и есть номер счёта.
Node label;
for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Номер счета")) {
label = node;
break;
}
}
if (label.y2() < 0) return {};
static const QRegularExpression digitsOnly(R"(^\d{10,}$)");
Node best;
for (const Node &node : m_nodes) {
if (node.y1() < label.y2()) continue;
if (!digitsOnly.match(node.contentDesc).hasMatch()) continue;
if (best.y1() < 0 || node.y1() < best.y1()) {
best = node;
}
}
return best.contentDesc;
}
bool ScreenXmlParser::isHistoryScreen() { bool ScreenXmlParser::isHistoryScreen() {
bool hasOperacii = false; bool hasOperacii = false;
bool hasVypiska = false; bool hasVypiska = false;
@ -527,32 +607,15 @@ Node ScreenXmlParser::findEditTextByHint(const QString &hint) {
} }
Node ScreenXmlParser::findSumEditText() { Node ScreenXmlParser::findSumEditText() {
// Якорь: View "Мин. сумма: ..." расположен под полем "Комментарий", // На экране перевода по карте порядок EditText'ов сверху вниз:
// которое следует за полем суммы. То есть над якорем два EditText'а: // [0] номер карты, [1] сумма, [2] комментарий.
// ближайший = "Комментарий", следующий вверх = "Сумма". QList<Node> editTexts;
Node anchor;
for (const Node &node : m_nodes) { for (const Node &node : m_nodes) {
if (node.contentDesc.contains(QString::fromUtf8("Мин. сумма"))) { if (node.className == "android.widget.EditText") editTexts.append(node);
anchor = node;
break;
}
} }
if (anchor.x1() < 0) return {}; std::sort(editTexts.begin(), editTexts.end(),
[](const Node &a, const Node &b) { return a.y1() < b.y1(); });
QList<Node> aboveAnchor; return editTexts.size() >= 2 ? editTexts[1] : Node{};
for (const Node &node : m_nodes) {
if (node.className != "android.widget.EditText") continue;
if (node.y2() > anchor.y1()) continue;
aboveAnchor.append(node);
}
// Сортируем по нижней границе по убыванию: [0] — ближайший (Комментарий),
// [1] — следующий выше (Сумма).
std::sort(aboveAnchor.begin(), aboveAnchor.end(),
[](const Node &a, const Node &b) { return a.y2() > b.y2(); });
if (aboveAnchor.size() >= 2) return aboveAnchor[1];
if (aboveAnchor.size() == 1) return aboveAnchor[0]; // нестандартная разметка
return {};
} }
Node ScreenXmlParser::tryToFindSystemPermissionDenyButton() { Node ScreenXmlParser::tryToFindSystemPermissionDenyButton() {

View File

@ -82,6 +82,24 @@ public:
// Парсит имя держателя с экрана детали // Парсит имя держателя с экрана детали
QString parseCardDetailHolder(); QString parseCardDetailHolder();
// Кнопка "Настройки карты" на экране детали карты
Node findCardSettingsButton();
// Bottom sheet с пунктами "Реквизиты карты" / "Заблокировать карту" и т.д.
bool isCardSettingsBottomSheet();
// Кнопка "Реквизиты карты" внутри bottom sheet "Настройки карты"
Node findCardRequisitesMenuItem();
// Экран "Реквизиты и информация" с табами "Реквизиты карты" / "Реквизиты счета"
bool isCardRequisitesScreen();
// Таб "Реквизиты счета" (правый) на экране "Реквизиты и информация"
Node findAccountRequisitesTab();
// Значение под лейблом "Номер счета" — длинная цифровая строка
QString parseAccountNumber();
// Определяет экран истории операций (табы "Операции" + "Выписка") // Определяет экран истории операций (табы "Операции" + "Выписка")
bool isHistoryScreen(); bool isHistoryScreen();

View File

@ -410,6 +410,105 @@ void GetCardInfoScript::doStart() {
} }
} }
// 2.5. Достаём номер счёта (общий для всех карт): Основной счёт → Посмотреть реквизиты
QString accountNumber;
{
bool needFetch = false;
for (const MaterialInfo &c : cards) {
const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers);
if (dbCard.id != -1 && dbCard.accountNumber.isEmpty()) { needFetch = true; break; }
}
if (needFetch) {
// Возвращаемся на главный экран — критерий именно по наличию кнопки
// "Основной счёт ... ₽". isHomeScreen() для Озона нестрогий
// (Перевести/Пополнить присутствуют и на других экранах).
Node mainAcc;
for (int attempt = 0; attempt < 6; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
mainAcc = xmlScreenParser.findMainAccountButton();
if (mainAcc.x() > 0) break;
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
}
if (mainAcc.x() > 0) {
AdbUtils::makeTap(m_deviceId, mainAcc.x(), mainAcc.y());
QThread::msleep(2000);
// Скроллим WebView, пока "Посмотреть реквизиты" не окажется в верхней половине,
// чтобы тап не ушёл в системный нав-бар.
Node showReq;
const int targetMaxY = device.screenHeight / 2;
for (int attempt = 0; attempt < 6; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
Node candidate = xmlScreenParser.findShowRequisitesButton();
if (candidate.x() > 0 && candidate.y() <= targetMaxY) {
showReq = candidate;
break;
}
AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2,
device.screenHeight * 2 / 3,
device.screenWidth / 2,
device.screenHeight / 3);
QThread::msleep(800);
}
if (showReq.x() > 0) {
AdbUtils::makeTap(m_deviceId, showReq.x(), showReq.y());
QThread::msleep(2000);
bool reqScreen = false;
for (int attempt = 0; attempt < 6; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isAccountRequisitesScreen()) {
reqScreen = true;
break;
}
QThread::msleep(1000);
}
if (reqScreen) {
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
accountNumber = xmlScreenParser.parseAccountNumberFromRequisites();
if (!accountNumber.isEmpty()) break;
QThread::msleep(800);
}
qDebug() << "[GetCardInfo] accountNumber:" << accountNumber;
} else {
qWarning() << "[GetCardInfo] Account requisites screen not detected";
}
} else {
qWarning() << "[GetCardInfo] 'Посмотреть реквизиты' not reached";
}
} else {
qWarning() << "[GetCardInfo] 'Основной счёт' button not found on home";
}
// Возврат на главный — критерий по кнопке "Основной счёт ... ₽"
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.findMainAccountButton().x() > 0) break;
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
}
// Записываем номер счёта во все материалы device/app
if (!accountNumber.isEmpty()) {
for (const MaterialInfo &c : cards) {
MaterialInfo upd = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers);
if (upd.id == -1) continue;
upd.accountNumber = accountNumber;
upd.updateTime = QDateTime::currentDateTimeUtc();
if (!MaterialDAO::upsertAccount(upd)) {
qWarning() << "[GetCardInfo] Failed to save accountNumber for" << upd.lastNumbers;
}
}
}
}
}
// 3. Отправляем материалы (карты) — после OCR, когда номера карт уже в БД // 3. Отправляем материалы (карты) — после OCR, когда номера карт уже в БД
const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode); const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode);
if (!updatedApp.bankProfileId.isEmpty()) { if (!updatedApp.bankProfileId.isEmpty()) {
@ -427,6 +526,7 @@ void GetCardInfoScript::doStart() {
QJsonObject materialBody; QJsonObject materialBody;
materialBody["bank_profile_id"] = updatedApp.bankProfileId; materialBody["bank_profile_id"] = updatedApp.bankProfileId;
materialBody["card_number"] = dbCard.cardNumber; materialBody["card_number"] = dbCard.cardNumber;
materialBody["account_number"] = dbCard.accountNumber;
materialBody["name"] = updatedApp.fullName; materialBody["name"] = updatedApp.fullName;
materialBody["state"] = materialStatusToString(dbCard.status) == "active" ? "enabled" : "disabled"; materialBody["state"] = materialStatusToString(dbCard.status) == "active" ? "enabled" : "disabled";
materialBody["currency"] = currencyCodes.value(dbCard.currency.toUpper(), 643); materialBody["currency"] = currencyCodes.value(dbCard.currency.toUpper(), 643);

View File

@ -400,6 +400,50 @@ Node ScreenXmlParser::findCardButtonByLastNumbers(const QString &lastNumbers) {
return {}; return {};
} }
Node ScreenXmlParser::findMainAccountButton() {
for (const Node &node : m_nodes) {
if (node.className != "android.widget.Button") continue;
QString t = node.text;
t.replace(QChar(0x00A0), ' ').replace(QChar(0x202F), ' ');
if (t.contains("Основной", Qt::CaseInsensitive) && t.contains("")) {
return node;
}
}
return {};
}
Node ScreenXmlParser::findShowRequisitesButton() {
for (const Node &node : m_nodes) {
if (node.text.trimmed() == QString::fromUtf8("Посмотреть реквизиты")) {
return node;
}
}
return {};
}
bool ScreenXmlParser::isAccountRequisitesScreen() {
for (const Node &node : m_nodes) {
if (node.text.trimmed() == QString::fromUtf8("Реквизиты счёта")) {
return true;
}
}
return false;
}
QString ScreenXmlParser::parseAccountNumberFromRequisites() {
// text="Номер счёта получателя 40914810100039708263"
// Префикс может варьироваться по регистру/пробелам — берём после первого совпадения цифр в конце.
static const QRegularExpression tailDigits(R"((\d{15,}))");
static const QString prefix = QString::fromUtf8("Номер счёта получателя");
for (const Node &node : m_nodes) {
const QString t = node.text;
if (!t.contains(prefix)) continue;
const QRegularExpressionMatch m = tailDigits.match(t);
if (m.hasMatch()) return m.captured(1);
}
return {};
}
QList<MaterialInfo> ScreenXmlParser::parseCardsOnHomeScreen() { QList<MaterialInfo> ScreenXmlParser::parseCardsOnHomeScreen() {
// "Основной счёт X ₽" — общая кнопка с балансом для всех карт. // "Основной счёт X ₽" — общая кнопка с балансом для всех карт.
// Карты — отдельные кнопки с текстом из 4 цифр (могут быть любые non-breaking spaces). // Карты — отдельные кнопки с текстом из 4 цифр (могут быть любые non-breaking spaces).

View File

@ -85,6 +85,18 @@ public:
// Finds a TextView with full card number (format "XXXX XXXX XXXX XXXX") // Finds a TextView with full card number (format "XXXX XXXX XXXX XXXX")
QString findFullCardNumber(); QString findFullCardNumber();
// Кнопка "Основной счёт X ₽" на главном экране
Node findMainAccountButton();
// Кнопка/текст "Посмотреть реквизиты" на экране "Основной счёт"
Node findShowRequisitesButton();
// Экран "Реквизиты счёта" — заголовок text="Реквизиты счёта"
bool isAccountRequisitesScreen();
// Парсит "Номер счёта получателя <20 цифр>" из текстовой ноды
QString parseAccountNumberFromRequisites();
bool isTransactionListScreen(); bool isTransactionListScreen();
// Возвращает кнопки транзакций на экране "Операции" (до maxCount штук) // Возвращает кнопки транзакций на экране "Операции" (до maxCount штук)

View File

@ -134,11 +134,14 @@ void DatabaseManager::initSchema() {
update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
status TEXT DEFAULT 'active', status TEXT DEFAULT 'active',
card_number TEXT DEFAULT '', card_number TEXT DEFAULT '',
account_number TEXT DEFAULT '',
material_id TEXT DEFAULT '', material_id TEXT DEFAULT '',
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, app_code, last_numbers) UNIQUE (device_id, app_code, last_numbers)
))"); ))");
q.exec("ALTER TABLE materials ADD COLUMN account_number TEXT DEFAULT ''");
q.exec(R"(CREATE TABLE IF NOT EXISTS transactions ( q.exec(R"(CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER, account_id INTEGER,

View File

@ -19,14 +19,15 @@ bool MaterialDAO::upsertAccount(const MaterialInfo &info) {
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( query.prepare(R"(
INSERT INTO materials (device_id, app_code, last_numbers, card_number, amount, update_time, status, description, currency, material_id) INSERT INTO materials (device_id, app_code, last_numbers, card_number, account_number, amount, update_time, status, description, currency, material_id)
VALUES (:device_id, :app_code, :last_numbers, :card_number, :amount, :update_time, :status, :description, :currency, :material_id) VALUES (:device_id, :app_code, :last_numbers, :card_number, :account_number, :amount, :update_time, :status, :description, :currency, :material_id)
ON CONFLICT(device_id, app_code, last_numbers) ON CONFLICT(device_id, app_code, last_numbers)
DO UPDATE SET DO UPDATE SET
amount = excluded.amount, amount = excluded.amount,
update_time = excluded.update_time, update_time = excluded.update_time,
last_numbers = excluded.last_numbers, last_numbers = excluded.last_numbers,
card_number = CASE WHEN excluded.card_number != '' THEN excluded.card_number ELSE materials.card_number END, card_number = CASE WHEN excluded.card_number != '' THEN excluded.card_number ELSE materials.card_number END,
account_number = CASE WHEN excluded.account_number != '' THEN excluded.account_number ELSE materials.account_number END,
status = excluded.status, status = excluded.status,
description = excluded.description, description = excluded.description,
currency = excluded.currency, currency = excluded.currency,
@ -37,6 +38,7 @@ bool MaterialDAO::upsertAccount(const MaterialInfo &info) {
query.bindValue(":app_code", info.appCode); query.bindValue(":app_code", info.appCode);
query.bindValue(":last_numbers", info.lastNumbers); query.bindValue(":last_numbers", info.lastNumbers);
query.bindValue(":card_number", info.cardNumber); query.bindValue(":card_number", info.cardNumber);
query.bindValue(":account_number", info.accountNumber);
query.bindValue(":currency", info.currency); query.bindValue(":currency", info.currency);
query.bindValue(":description", info.description); query.bindValue(":description", info.description);
query.bindValue(":amount", info.amount); query.bindValue(":amount", info.amount);
@ -133,6 +135,7 @@ MaterialInfo parseAccount(const QSqlQuery &query) {
acc.appCode = query.value("app_code").toString(); acc.appCode = query.value("app_code").toString();
acc.lastNumbers = query.value("last_numbers").toString(); acc.lastNumbers = query.value("last_numbers").toString();
acc.cardNumber = query.value("card_number").toString(); acc.cardNumber = query.value("card_number").toString();
acc.accountNumber = query.value("account_number").toString();
acc.materialId = query.value("material_id").toString(); acc.materialId = query.value("material_id").toString();
acc.amount = query.value("amount").toDouble(); acc.amount = query.value("amount").toDouble();
acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString()); acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());
@ -151,7 +154,7 @@ QList<MaterialInfo> MaterialDAO::activateExclusive(
QSqlQuery sel(db); QSqlQuery sel(db);
sel.prepare(R"( sel.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency
FROM materials FROM materials
WHERE device_id = :device_id AND app_code = :app_code WHERE device_id = :device_id AND app_code = :app_code
AND status = 'active' AND id != :target_id AND status = 'active' AND id != :target_id
@ -199,7 +202,7 @@ QList<MaterialInfo> MaterialDAO::getAccounts(const QString &deviceId, const QStr
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency
FROM materials FROM materials
WHERE device_id = :device_id AND app_code = :app_code WHERE device_id = :device_id AND app_code = :app_code
)"); )");
@ -223,7 +226,7 @@ MaterialInfo MaterialDAO::getAccountById(const int accountId) {
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency
FROM materials FROM materials
WHERE id = :id WHERE id = :id
LIMIT 1 LIMIT 1
@ -247,7 +250,7 @@ QList<MaterialInfo> MaterialDAO::getAccountsByAppCode(const QString &appCode) {
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency
FROM materials FROM materials
WHERE app_code = :app_code WHERE app_code = :app_code
)"); )");
@ -270,7 +273,7 @@ MaterialInfo MaterialDAO::findAppAccount(const QString &deviceId, const QString
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency
FROM materials FROM materials
WHERE app_code = :appName AND last_numbers = :lastNumber AND device_id = :deviceId WHERE app_code = :appName AND last_numbers = :lastNumber AND device_id = :deviceId
LIMIT 1 LIMIT 1
@ -334,7 +337,7 @@ MaterialInfo MaterialDAO::findByMaterialId(const QString &materialId) {
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency
FROM materials FROM materials
WHERE material_id = :material_id WHERE material_id = :material_id
LIMIT 1 LIMIT 1

View File

@ -53,6 +53,7 @@ CREATE TABLE IF NOT EXISTS materials
update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
status TEXT DEFAULT 'active', status TEXT DEFAULT 'active',
card_number TEXT DEFAULT '', card_number TEXT DEFAULT '',
account_number TEXT DEFAULT '',
material_id TEXT DEFAULT '', material_id TEXT DEFAULT '',
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, app_code, last_numbers) UNIQUE (device_id, app_code, last_numbers)

View File

@ -35,6 +35,7 @@ struct MaterialInfo {
QString appCode; QString appCode;
QString lastNumbers; QString lastNumbers;
QString cardNumber; // full 16-digit card number QString cardNumber; // full 16-digit card number
QString accountNumber; // full bank account number (IBAN-like)
QString materialId; QString materialId;
QString currency; QString currency;
QString description; QString description;