#include "DatabaseManager.h" #include #include #include #include #include #include #include DatabaseManager::DatabaseManager() = default; DatabaseManager::~DatabaseManager() { QMutexLocker locker(&mutex); for (auto it = connectionPool.begin(); it != connectionPool.end(); ++it) { QSqlDatabase::removeDatabase(it.key()); } 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(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"; } } void DatabaseManager::closeConnection() { QMutexLocker locker(&mutex); if (const QString name = connectionName(); connectionPool.contains(name)) { connectionPool.remove(name); QSqlDatabase::removeDatabase(name); qDebug() << "Соединение" << name << "удалено вручную"; } }