Implemented database migration for adding new fields (`json`, `sent_to_server`) in `events` table. Updated DAO methods to support the new fields and introduced `markSentToServer`. Enhanced Ozon transaction workflows with OCR-based extraction, event error handling improvements, and result reporting. Refactored `EventHandler` for streamlined task processing and stale event cleanup.
384 lines
14 KiB
C++
384 lines
14 KiB
C++
#include "DatabaseManager.h"
|
||
#include <QCoreApplication>
|
||
#include <QSqlError>
|
||
#include <QSqlQuery>
|
||
#include <QDebug>
|
||
#include <QDir>
|
||
#include <QSettings>
|
||
#include <QThread>
|
||
|
||
DatabaseManager::DatabaseManager() = default;
|
||
|
||
DatabaseManager::~DatabaseManager() {
|
||
QMutexLocker locker(&mutex);
|
||
|
||
// QSqlDatabase требует живого QCoreApplication — при завершении
|
||
// статический синглтон может разрушаться после QApplication
|
||
if (!QCoreApplication::instance()) {
|
||
connectionPool.clear();
|
||
return;
|
||
}
|
||
|
||
for (auto it = connectionPool.begin(); it != connectionPool.end(); ++it) {
|
||
QSqlDatabase::removeDatabase(it.key());
|
||
}
|
||
connectionPool.clear();
|
||
qDebug() << "Все соединения закрыты";
|
||
}
|
||
|
||
DatabaseManager &DatabaseManager::instance() {
|
||
static DatabaseManager instance;
|
||
return instance;
|
||
}
|
||
|
||
QSqlDatabase DatabaseManager::database() {
|
||
QMutexLocker locker(&mutex);
|
||
QString name = connectionName();
|
||
|
||
// Создаем новое соединение, если его ещё нет
|
||
if (!connectionPool.contains(name)) {
|
||
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", name);
|
||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||
|
||
QString dbPath = settings.value("db/dbPath").toString();
|
||
if (dbPath.startsWith("~")) {
|
||
dbPath.replace(0, 1, QDir::homePath());
|
||
}
|
||
|
||
// qDebug() << settings.value("db/dbPath").toString();
|
||
// db.setDatabaseName(QCoreApplication::applicationDirPath() + "/database/app_data.db");
|
||
db.setDatabaseName(dbPath);
|
||
|
||
if (!db.open()) {
|
||
qWarning() << "Ошибка открытия базы данных:" << db.lastError().text();
|
||
}
|
||
|
||
connectionPool[name] = db;
|
||
|
||
// Автоматическое удаление соединения при завершении потока
|
||
QObject::connect(QThread::currentThread(), &QThread::finished, [this, name]() {
|
||
QMutexLocker cLocker(&mutex);
|
||
if (connectionPool.contains(name)) {
|
||
connectionPool.remove(name);
|
||
QSqlDatabase::removeDatabase(name);
|
||
qDebug() << "Соединение" << name << "удалено";
|
||
}
|
||
});
|
||
}
|
||
|
||
return connectionPool[name];
|
||
}
|
||
|
||
QString DatabaseManager::connectionName() {
|
||
return QString("connection_%1").arg(reinterpret_cast<quintptr>(QThread::currentThreadId()));
|
||
}
|
||
|
||
|
||
static bool columnExists(QSqlQuery &q, const QString &table, const QString &column) {
|
||
q.exec(QString("SELECT COUNT(*) FROM pragma_table_info('%1') WHERE name='%2'")
|
||
.arg(table, column));
|
||
return q.next() && q.value(0).toInt() > 0;
|
||
}
|
||
|
||
void DatabaseManager::initSchema() {
|
||
QSqlDatabase db = database();
|
||
QSqlQuery q(db);
|
||
|
||
// Читаем текущую версию схемы
|
||
q.exec("PRAGMA user_version");
|
||
const int version = q.next() ? q.value(0).toInt() : 0;
|
||
|
||
// ── Версия 1: начальная схема + api_id для devices ────────────────────────
|
||
if (version < 1) {
|
||
db.transaction();
|
||
|
||
q.exec(R"(CREATE TABLE IF NOT EXISTS devices (
|
||
id TEXT PRIMARY KEY,
|
||
api_id TEXT,
|
||
status TEXT,
|
||
name TEXT,
|
||
android TEXT,
|
||
screen_width INTEGER,
|
||
screen_height INTEGER,
|
||
battery INTEGER,
|
||
update_time DATETIME,
|
||
timestamp DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
|
||
image BLOB
|
||
))");
|
||
|
||
q.exec(R"(CREATE TABLE IF NOT EXISTS applications (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
device_id TEXT,
|
||
pin_code TEXT,
|
||
pin_code_checked TEXT DEFAULT 'no',
|
||
name TEXT,
|
||
code TEXT,
|
||
package TEXT,
|
||
status TEXT DEFAULT 'off',
|
||
install TEXT DEFAULT 'no',
|
||
comment TEXT,
|
||
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
|
||
UNIQUE (device_id, code)
|
||
))");
|
||
|
||
q.exec(R"(CREATE TABLE IF NOT EXISTS accounts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
device_id TEXT,
|
||
app_code TEXT,
|
||
last_numbers TEXT,
|
||
description TEXT,
|
||
amount REAL,
|
||
currency TEXT DEFAULT '',
|
||
update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
|
||
status TEXT DEFAULT 'active',
|
||
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
|
||
UNIQUE (device_id, app_code, last_numbers)
|
||
))");
|
||
|
||
q.exec(R"(CREATE TABLE IF NOT EXISTS transactions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
account_id INTEGER,
|
||
external_id INTEGER,
|
||
amount REAL,
|
||
fee REAL,
|
||
status TEXT,
|
||
type TEXT,
|
||
phone TEXT,
|
||
name TEXT,
|
||
short_description TEXT,
|
||
updated_short_description TEXT,
|
||
description TEXT,
|
||
updated_description TEXT,
|
||
bank_name TEXT,
|
||
bank_time TEXT,
|
||
bank_tr_external_id TEXT,
|
||
update_time DATETIME,
|
||
complete_time DATETIME,
|
||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
|
||
))");
|
||
|
||
q.exec(R"(CREATE TABLE IF NOT EXISTS events (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
account_id INTEGER,
|
||
external_id INTEGER,
|
||
device_id TEXT,
|
||
amount REAL,
|
||
phone TEXT,
|
||
bank_name TEXT,
|
||
comment TEXT,
|
||
status TEXT,
|
||
type TEXT,
|
||
update_time DATETIME,
|
||
complete_time DATETIME,
|
||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
|
||
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
|
||
UNIQUE (external_id)
|
||
))");
|
||
|
||
// Добавляем api_id в devices если ещё нет (для существующих БД)
|
||
if (!columnExists(q, "devices", "api_id")) {
|
||
q.exec("ALTER TABLE devices ADD COLUMN api_id TEXT");
|
||
}
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 1");
|
||
qDebug() << "[DB] Migration 1 applied";
|
||
}
|
||
|
||
// ── Версия 2: currency в accounts + переименование app_name → app_code ───
|
||
if (version < 2) {
|
||
db.transaction();
|
||
|
||
// Добавляем currency если ещё нет (существующие БД без этого поля)
|
||
if (!columnExists(q, "accounts", "currency")) {
|
||
q.exec("ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT ''");
|
||
}
|
||
|
||
// Переименовываем app_name → app_code (SQLite 3.25+)
|
||
if (columnExists(q, "accounts", "app_name")) {
|
||
q.exec("ALTER TABLE accounts RENAME COLUMN app_name TO app_code");
|
||
if (q.lastError().isValid()) {
|
||
qWarning() << "[DB] Migration 2: rename app_name failed:" << q.lastError().text();
|
||
}
|
||
}
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 2");
|
||
qDebug() << "[DB] Migration 2 applied";
|
||
}
|
||
|
||
// ── Версия 3: phone и full_name в applications ────────────────────────────
|
||
if (version < 3) {
|
||
db.transaction();
|
||
|
||
if (!columnExists(q, "applications", "phone")) {
|
||
q.exec("ALTER TABLE applications ADD COLUMN phone TEXT DEFAULT ''");
|
||
}
|
||
if (!columnExists(q, "applications", "full_name")) {
|
||
q.exec("ALTER TABLE applications ADD COLUMN full_name TEXT DEFAULT ''");
|
||
}
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 3");
|
||
qDebug() << "[DB] Migration 3 applied";
|
||
}
|
||
|
||
// ── Версия 4: card_number в accounts ──────────────────────────────────────
|
||
if (version < 4) {
|
||
db.transaction();
|
||
|
||
if (!columnExists(q, "accounts", "card_number")) {
|
||
q.exec("ALTER TABLE accounts ADD COLUMN card_number TEXT DEFAULT ''");
|
||
}
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 4");
|
||
qDebug() << "[DB] Migration 4 applied";
|
||
}
|
||
|
||
// ── Версия 5: bank_profile_id в applications ──────────────────────────────
|
||
if (version < 5) {
|
||
db.transaction();
|
||
|
||
if (!columnExists(q, "applications", "bank_profile_id")) {
|
||
q.exec("ALTER TABLE applications ADD COLUMN bank_profile_id TEXT DEFAULT ''");
|
||
}
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 5");
|
||
qDebug() << "[DB] Migration 5 applied";
|
||
}
|
||
|
||
// ── Версия 6: currency и amount в applications ─────────────────────────────
|
||
if (version < 6) {
|
||
db.transaction();
|
||
|
||
if (!columnExists(q, "applications", "currency")) {
|
||
q.exec("ALTER TABLE applications ADD COLUMN currency TEXT DEFAULT ''");
|
||
}
|
||
if (!columnExists(q, "applications", "amount")) {
|
||
q.exec("ALTER TABLE applications ADD COLUMN amount REAL DEFAULT 0");
|
||
}
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 6");
|
||
qDebug() << "[DB] Migration 6 applied";
|
||
}
|
||
|
||
// ── Версия 7: material_id в accounts ──────────────────────────────────────
|
||
if (version < 7) {
|
||
db.transaction();
|
||
|
||
if (!columnExists(q, "accounts", "material_id")) {
|
||
q.exec("ALTER TABLE accounts ADD COLUMN material_id TEXT DEFAULT ''");
|
||
}
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 7");
|
||
qDebug() << "[DB] Migration 7 applied";
|
||
}
|
||
|
||
// ── Версия 8: receipt_image в transactions ──────────────────────────────────
|
||
if (version < 8) {
|
||
db.transaction();
|
||
|
||
if (!columnExists(q, "transactions", "receipt_image")) {
|
||
q.exec("ALTER TABLE transactions ADD COLUMN receipt_image BLOB");
|
||
}
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 8");
|
||
qDebug() << "[DB] Migration 8 applied";
|
||
}
|
||
|
||
// ── Версия 9: таблица banks ───────────────────────────────────────────────
|
||
if (version < 9) {
|
||
db.transaction();
|
||
|
||
q.exec(R"(CREATE TABLE IF NOT EXISTS banks (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
alias TEXT UNIQUE,
|
||
name TEXT,
|
||
nspk_name TEXT
|
||
))");
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 9");
|
||
qDebug() << "[DB] Migration 9 applied";
|
||
}
|
||
|
||
// ── Версия 10: таблица app_logs ───────────────────────────────────────────
|
||
if (version < 10) {
|
||
db.transaction();
|
||
|
||
q.exec(R"(CREATE TABLE IF NOT EXISTS app_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
source TEXT NOT NULL DEFAULT '',
|
||
device_id TEXT NOT NULL DEFAULT '',
|
||
message TEXT NOT NULL,
|
||
screenshot BLOB,
|
||
timestamp TEXT NOT NULL
|
||
))");
|
||
|
||
q.exec("CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC)");
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 10");
|
||
qDebug() << "[DB] Migration 10 applied";
|
||
}
|
||
|
||
// ── Версия 11: убираем UNIQUE(external_id) из events ─────────────────────
|
||
if (version < 11) {
|
||
db.transaction();
|
||
|
||
q.exec(R"(CREATE TABLE IF NOT EXISTS events_new (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
account_id INTEGER,
|
||
external_id TEXT,
|
||
device_id TEXT,
|
||
amount REAL,
|
||
phone TEXT,
|
||
bank_name TEXT,
|
||
comment TEXT,
|
||
status TEXT,
|
||
type TEXT,
|
||
update_time DATETIME,
|
||
complete_time DATETIME,
|
||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
|
||
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE
|
||
))");
|
||
|
||
q.exec(R"(INSERT INTO events_new
|
||
SELECT * FROM events)");
|
||
|
||
q.exec("DROP TABLE events");
|
||
q.exec("ALTER TABLE events_new RENAME TO events");
|
||
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 11");
|
||
qDebug() << "[DB] Migration 11 applied";
|
||
}
|
||
|
||
// ── Версия 12: json и sent_to_server в events ────────────────────────────
|
||
if (version < 12) {
|
||
db.transaction();
|
||
q.exec("ALTER TABLE events ADD COLUMN json TEXT DEFAULT ''");
|
||
q.exec("ALTER TABLE events ADD COLUMN sent_to_server INTEGER DEFAULT 0");
|
||
db.commit();
|
||
q.exec("PRAGMA user_version = 12");
|
||
qDebug() << "[DB] Migration 12 applied";
|
||
}
|
||
}
|
||
|
||
void DatabaseManager::closeConnection() {
|
||
QMutexLocker locker(&mutex);
|
||
if (const QString name = connectionName(); connectionPool.contains(name)) {
|
||
connectionPool.remove(name);
|
||
QSqlDatabase::removeDatabase(name);
|
||
qDebug() << "Соединение" << name << "удалено вручную";
|
||
}
|
||
}
|