mnemo_cards/mnemo_cards_backend/lib/database/database.dart

420 lines
13 KiB
Dart
Raw Normal View History

2025-12-13 13:27:05 +00:00
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:postgres/postgres.dart' as pg;
import 'dart:io';
2025-12-17 00:41:47 +00:00
import 'dart:convert';
2025-12-13 13:27:05 +00:00
// Импорт конвертеров (нужен для генерации кода)
import 'converters.dart';
// Импорт всех таблиц
import 'tables/users.dart';
import 'tables/auth.dart';
import 'tables/packs.dart';
import 'tables/relations.dart';
import 'tables/subscriptions.dart';
import 'tables/payments.dart';
import 'tables/tests.dart';
import 'tables/tasks.dart';
import 'tables/promo_codes.dart';
import 'tables/discounts.dart';
import 'tables/statistics.dart';
import 'tables/telegram.dart';
import 'tables/achievements.dart';
2025-12-14 20:36:05 +00:00
import 'tables/word_statistics.dart';
import 'tables/audit.dart';
2025-12-13 13:27:05 +00:00
// Импорт DAOs
import 'daos/user_dao.dart';
import 'daos/pack_dao.dart';
import 'daos/test_dao.dart';
import 'daos/payment_dao.dart';
import 'daos/subscription_dao.dart';
import 'daos/task_dao.dart';
import 'daos/promo_code_dao.dart';
import 'daos/discount_dao.dart';
import 'daos/statistics_dao.dart';
import 'daos/achievement_dao.dart';
2025-12-14 20:36:05 +00:00
import 'daos/word_statistics_dao.dart';
import 'daos/audit_dao.dart';
2025-12-13 13:27:05 +00:00
// Сгенерированный код будет здесь
part 'database.g.dart';
@DriftDatabase(
tables: [
// User tables
Users,
UserDatas,
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Auth tables
Tokens,
RefreshTokens,
TelegramAuthCodes,
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Pack tables
CardPacks,
GameCards,
VoiceModels,
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Relations
UserPacks,
PreviewCards,
CardPackCards,
CardVoices,
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Subscription tables
SubscriptionPlans,
UserSubscriptions,
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Payment tables
Payments,
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Test tables
Tests,
TestQuestions,
TestPackRelations,
TestStatistics,
// Task tables
Tasks,
UserTasks,
UserTaskProgresses,
UserTaskResults,
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Promo code tables
PromoCodesCampaigns,
PromoCodes,
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Discount tables
DiscountCampaigns,
Discounts,
DiscountUserDatas,
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Statistics tables
StudySessions,
2025-12-14 20:36:05 +00:00
WordStatistics,
2025-12-13 13:27:05 +00:00
// Achievements tables
UserAchievements,
// Telegram tables
ShareRequests,
2025-12-20 18:26:15 +00:00
2025-12-14 20:36:05 +00:00
// Audit tables
AuditLogs,
2025-12-13 13:27:05 +00:00
],
daos: [
UserDao,
PackDao,
TestDao,
PaymentDao,
SubscriptionDao,
TaskDao,
PromoCodeDao,
DiscountDao,
StatisticsDao,
AchievementDao,
2025-12-14 20:36:05 +00:00
WordStatisticsDao,
AuditDao,
2025-12-13 13:27:05 +00:00
],
)
class AppDatabase extends _$AppDatabase {
AppDatabase(super.e);
@override
2025-12-18 20:40:48 +00:00
int get schemaVersion => 3;
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Factory для подключения к PostgreSQL
static AppDatabase connect({
required String host,
required int port,
required String database,
required String username,
required String password,
bool useSsl = false,
}) {
final endpoint = pg.Endpoint(
host: host,
port: port,
database: database,
username: username,
password: password,
);
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
final connection = PgDatabase(
endpoint: endpoint,
settings: pg.ConnectionSettings(
sslMode: useSsl ? pg.SslMode.require : pg.SslMode.disable,
connectTimeout: const Duration(seconds: 10),
),
);
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
return AppDatabase(connection);
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Factory для подключения из environment variables
static AppDatabase fromEnvironment() {
return connect(
host: Platform.environment['DB_HOST'] ?? 'localhost',
port: int.parse(Platform.environment['DB_PORT'] ?? '5432'),
database: Platform.environment['DB_NAME'] ?? 'mnemo_cards_dev',
username: Platform.environment['DB_USER'] ?? 'mnemo_user',
password: Platform.environment['DB_PASSWORD'] ?? '',
useSsl: Platform.environment['DB_SSL_MODE'] == 'require',
);
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) async {
print('Creating database schema...');
await m.createAll();
print('Database schema created successfully');
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Создать индексы для оптимизации
await _createIndexes();
},
onUpgrade: (Migrator m, int from, int to) async {
print('Migrating database from version $from to $to');
2025-12-20 18:26:15 +00:00
2025-12-17 00:41:47 +00:00
// Миграция с версии 1 на 2: обновление структуры тестов
if (from < 2) {
await _migrateToV2(m);
}
2025-12-18 20:40:48 +00:00
// Миграция с версии 2 на 3: добавление telegram в users
if (from < 3) {
await _migrateToV3(m);
}
2025-12-13 13:27:05 +00:00
},
beforeOpen: (details) async {
print('Opening database connection...');
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Проверка подключения
final result = await customSelect('SELECT 1 as test').getSingle();
print('Database connection successful: ${result.data}');
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Включить foreign key constraints
await customStatement('SET CONSTRAINTS ALL IMMEDIATE');
},
);
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Создание индексов для оптимизации запросов
Future<void> _createIndexes() async {
print('Creating indexes...');
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// Users indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_users_email ON users(email) WHERE email IS NOT NULL',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_users_telegram ON users(telegram) WHERE telegram IS NOT NULL',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_users_admin ON users(admin) WHERE admin = TRUE',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_users_not_deleted ON users(is_deleted) WHERE is_deleted = FALSE',
);
2025-12-13 13:27:05 +00:00
// UserDatas indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_user_datas_user_id ON user_datas(user_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_user_datas_last_online ON user_datas(last_time_online DESC)',
);
2025-12-13 13:27:05 +00:00
// Auth indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_tokens_user_id ON tokens(user_id)',
);
2025-12-13 22:07:44 +00:00
// Индекс на expires без предиката (NOW() не может быть использована в предикате индекса)
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_tokens_expires ON tokens(expires)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_refresh_tokens_jti ON refresh_tokens(jti)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_refresh_tokens_not_blacklisted ON refresh_tokens(is_blacklisted) WHERE is_blacklisted = FALSE',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_telegram_codes_code ON telegram_auth_codes(code)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_telegram_codes_not_used ON telegram_auth_codes(is_used) WHERE is_used = FALSE',
);
2025-12-13 13:27:05 +00:00
// CardPacks indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_packs_enabled ON card_packs(enabled) WHERE enabled = TRUE',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_packs_order ON card_packs("order")',
);
2025-12-13 13:27:05 +00:00
// GameCards indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_cards_original ON game_cards(original)',
);
2025-12-13 13:27:05 +00:00
// UserPacks indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_user_packs_user_id ON user_packs(user_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_user_packs_pack_id ON user_packs(pack_id)',
);
2025-12-13 13:27:05 +00:00
// Payments indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_payments_user_id ON payments(user_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_payments_created ON payments(date DESC)',
);
2025-12-13 13:27:05 +00:00
// Subscriptions indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON user_subscriptions(user_id)',
);
2025-12-13 22:07:44 +00:00
// Индекс на finish без предиката (NOW() не может быть использована в предикате индекса)
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_subscriptions_finish ON user_subscriptions(finish)',
);
2025-12-13 13:27:05 +00:00
// Tests indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_test_questions_test_id ON test_questions(test_id)',
);
2025-12-13 13:27:05 +00:00
// StudySessions indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON study_sessions(user_id)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_sessions_started ON study_sessions(start_time DESC)',
);
2025-12-13 13:27:05 +00:00
// PromoCodes indexes
2025-12-20 18:26:15 +00:00
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_promo_codes_code ON promo_codes(code)',
);
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_promo_codes_campaign_id ON promo_codes(campaign_id)',
);
2025-12-13 13:27:05 +00:00
print('Indexes created successfully');
}
2025-12-17 00:41:47 +00:00
/// Миграция с версии 1 на версию 2
/// Обновление структуры тестов: добавление новых колонок и очистка старых данных
Future<void> _migrateToV2(Migrator m) async {
2025-12-20 18:26:15 +00:00
print(
'Starting migration to v2: updating test questions and statistics...',
);
2025-12-17 00:41:47 +00:00
try {
// 1. Добавляем новые колонки в test_questions
print('Adding new columns to test_questions...');
await customStatement(
'ALTER TABLE test_questions '
'ADD COLUMN IF NOT EXISTS order_index INTEGER DEFAULT 0, '
'ADD COLUMN IF NOT EXISTS word TEXT DEFAULT \'\', '
'ADD COLUMN IF NOT EXISTS answer TEXT DEFAULT \'\', '
'ADD COLUMN IF NOT EXISTS options TEXT DEFAULT \'[]\', '
'ADD COLUMN IF NOT EXISTS ui_data TEXT DEFAULT \'{}\'',
);
// 2. Удаляем все старые вопросы (soft delete)
print('Soft-deleting all existing test questions...');
await customStatement(
'UPDATE test_questions SET is_deleted = TRUE, deleted_at = NOW() WHERE is_deleted = FALSE',
);
2025-12-20 18:26:15 +00:00
print(
'All old questions marked as deleted. Create new questions via admin panel.',
);
2025-12-17 00:41:47 +00:00
// 3. Удаляем старую колонку body
print('Dropping old body column...');
2025-12-20 18:26:15 +00:00
await customStatement(
'ALTER TABLE test_questions DROP COLUMN IF EXISTS body',
);
2025-12-17 00:41:47 +00:00
// 4. Добавляем новые колонки в test_statistics
print('Adding new columns to test_statistics...');
await customStatement(
'ALTER TABLE test_statistics '
'ADD COLUMN IF NOT EXISTS total_questions INTEGER DEFAULT 0, '
'ADD COLUMN IF NOT EXISTS correct_answers INTEGER DEFAULT 0, '
'ADD COLUMN IF NOT EXISTS incorrect_answers INTEGER DEFAULT 0, '
'ADD COLUMN IF NOT EXISTS skipped_answers INTEGER DEFAULT 0, '
'ADD COLUMN IF NOT EXISTS score_percentage REAL DEFAULT 0.0, '
'ADD COLUMN IF NOT EXISTS time_spent_seconds INTEGER, '
'ADD COLUMN IF NOT EXISTS question_results TEXT DEFAULT \'[]\', '
'ADD COLUMN IF NOT EXISTS metadata TEXT DEFAULT \'{}\'',
);
// 5. Удаляем всю старую статистику (она будет создаваться заново)
print('Deleting old test statistics...');
await customStatement('DELETE FROM test_statistics');
2025-12-20 18:26:15 +00:00
print(
'All old statistics deleted. New statistics will be collected automatically.',
);
2025-12-17 00:41:47 +00:00
// 6. Удаляем старую колонку results
print('Dropping old results column...');
2025-12-20 18:26:15 +00:00
await customStatement(
'ALTER TABLE test_statistics DROP COLUMN IF EXISTS results',
);
2025-12-17 00:41:47 +00:00
print('Migration to v2 completed successfully!');
} catch (e, stackTrace) {
print('Error during migration to v2: $e');
print('Stack trace: $stackTrace');
rethrow;
}
}
2025-12-18 20:40:48 +00:00
/// Миграция с версии 2 на версию 3
/// Добавление поля telegram в users и перенос старых telegram-логинов из email
Future<void> _migrateToV3(Migrator m) async {
print('Starting migration to v3: adding telegram to users...');
try {
await customStatement(
'ALTER TABLE users ADD COLUMN IF NOT EXISTS telegram TEXT',
);
// Раньше Telegram-username сохранялся в email. Переносим "похожие на username"
// значения в telegram и очищаем email.
await customStatement(
'UPDATE users '
'SET telegram = email, email = NULL '
'WHERE (telegram IS NULL OR telegram = \'\') '
'AND email IS NOT NULL AND email != \'\' '
'AND POSITION(\'@\' IN email) = 0',
);
// Индексы на existing db
await customStatement(
'CREATE INDEX IF NOT EXISTS idx_users_telegram ON users(telegram) WHERE telegram IS NOT NULL',
);
print('Migration to v3 completed successfully!');
} catch (e, stackTrace) {
print('Error during migration to v3: $e');
print('Stack trace: $stackTrace');
rethrow;
}
}
2025-12-13 13:27:05 +00:00
}