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';
|
|
|
|
|
|
|
|
|
|
// Импорт конвертеров (нужен для генерации кода)
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
// Импорт 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';
|
|
|
|
|
|
|
|
|
|
// Сгенерированный код будет здесь
|
|
|
|
|
part 'database.g.dart';
|
|
|
|
|
|
|
|
|
|
@DriftDatabase(
|
|
|
|
|
tables: [
|
|
|
|
|
// User tables
|
|
|
|
|
Users,
|
|
|
|
|
UserDatas,
|
|
|
|
|
|
|
|
|
|
// Auth tables
|
|
|
|
|
Tokens,
|
|
|
|
|
RefreshTokens,
|
|
|
|
|
TelegramAuthCodes,
|
|
|
|
|
|
|
|
|
|
// Pack tables
|
|
|
|
|
CardPacks,
|
|
|
|
|
GameCards,
|
|
|
|
|
VoiceModels,
|
|
|
|
|
|
|
|
|
|
// Relations
|
|
|
|
|
UserPacks,
|
|
|
|
|
PreviewCards,
|
|
|
|
|
CardPackCards,
|
|
|
|
|
CardVoices,
|
|
|
|
|
|
|
|
|
|
// Subscription tables
|
|
|
|
|
SubscriptionPlans,
|
|
|
|
|
UserSubscriptions,
|
|
|
|
|
|
|
|
|
|
// Payment tables
|
|
|
|
|
Payments,
|
|
|
|
|
|
|
|
|
|
// Test tables
|
|
|
|
|
Tests,
|
|
|
|
|
TestQuestions,
|
|
|
|
|
TestPackRelations,
|
|
|
|
|
TestStatistics,
|
|
|
|
|
|
|
|
|
|
// Task tables
|
|
|
|
|
Tasks,
|
|
|
|
|
UserTasks,
|
|
|
|
|
UserTaskProgresses,
|
|
|
|
|
UserTaskResults,
|
|
|
|
|
|
|
|
|
|
// Promo code tables
|
|
|
|
|
PromoCodesCampaigns,
|
|
|
|
|
PromoCodes,
|
|
|
|
|
|
|
|
|
|
// Discount tables
|
|
|
|
|
DiscountCampaigns,
|
|
|
|
|
Discounts,
|
|
|
|
|
DiscountUserDatas,
|
|
|
|
|
|
|
|
|
|
// Statistics tables
|
|
|
|
|
StudySessions,
|
|
|
|
|
|
|
|
|
|
// Achievements tables
|
|
|
|
|
UserAchievements,
|
|
|
|
|
|
|
|
|
|
// Telegram tables
|
|
|
|
|
ShareRequests,
|
|
|
|
|
],
|
|
|
|
|
daos: [
|
|
|
|
|
UserDao,
|
|
|
|
|
PackDao,
|
|
|
|
|
TestDao,
|
|
|
|
|
PaymentDao,
|
|
|
|
|
SubscriptionDao,
|
|
|
|
|
TaskDao,
|
|
|
|
|
PromoCodeDao,
|
|
|
|
|
DiscountDao,
|
|
|
|
|
StatisticsDao,
|
|
|
|
|
AchievementDao,
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
class AppDatabase extends _$AppDatabase {
|
|
|
|
|
AppDatabase(super.e);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
int get schemaVersion => 1;
|
|
|
|
|
|
|
|
|
|
/// 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,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final connection = PgDatabase(
|
|
|
|
|
endpoint: endpoint,
|
|
|
|
|
settings: pg.ConnectionSettings(
|
|
|
|
|
sslMode: useSsl ? pg.SslMode.require : pg.SslMode.disable,
|
|
|
|
|
connectTimeout: const Duration(seconds: 10),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return AppDatabase(connection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
MigrationStrategy get migration => MigrationStrategy(
|
|
|
|
|
onCreate: (Migrator m) async {
|
|
|
|
|
print('Creating database schema...');
|
|
|
|
|
await m.createAll();
|
|
|
|
|
print('Database schema created successfully');
|
|
|
|
|
|
|
|
|
|
// Создать индексы для оптимизации
|
|
|
|
|
await _createIndexes();
|
|
|
|
|
},
|
|
|
|
|
onUpgrade: (Migrator m, int from, int to) async {
|
|
|
|
|
print('Migrating database from version $from to $to');
|
|
|
|
|
|
|
|
|
|
// Миграции при обновлении схемы
|
|
|
|
|
// if (from < 2) {
|
|
|
|
|
// await m.addColumn(users, users.phoneNumber);
|
|
|
|
|
// }
|
|
|
|
|
},
|
|
|
|
|
beforeOpen: (details) async {
|
|
|
|
|
print('Opening database connection...');
|
|
|
|
|
|
|
|
|
|
// Проверка подключения
|
|
|
|
|
final result = await customSelect('SELECT 1 as test').getSingle();
|
|
|
|
|
print('Database connection successful: ${result.data}');
|
|
|
|
|
|
|
|
|
|
// Включить foreign key constraints
|
|
|
|
|
await customStatement('SET CONSTRAINTS ALL IMMEDIATE');
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/// Создание индексов для оптимизации запросов
|
|
|
|
|
Future<void> _createIndexes() async {
|
|
|
|
|
print('Creating indexes...');
|
|
|
|
|
|
|
|
|
|
// Users indexes
|
|
|
|
|
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_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');
|
|
|
|
|
|
|
|
|
|
// UserDatas indexes
|
|
|
|
|
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)');
|
|
|
|
|
|
|
|
|
|
// Auth indexes
|
|
|
|
|
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() не может быть использована в предикате индекса)
|
|
|
|
|
await customStatement('CREATE INDEX IF NOT EXISTS idx_tokens_expires ON tokens(expires)');
|
2025-12-13 13:27:05 +00:00
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
// CardPacks indexes
|
|
|
|
|
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")');
|
|
|
|
|
|
|
|
|
|
// GameCards indexes
|
|
|
|
|
await customStatement('CREATE INDEX IF NOT EXISTS idx_cards_original ON game_cards(original)');
|
|
|
|
|
|
|
|
|
|
// UserPacks indexes
|
|
|
|
|
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)');
|
|
|
|
|
|
|
|
|
|
// Payments indexes
|
|
|
|
|
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)');
|
|
|
|
|
|
|
|
|
|
// Subscriptions indexes
|
|
|
|
|
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() не может быть использована в предикате индекса)
|
|
|
|
|
await customStatement('CREATE INDEX IF NOT EXISTS idx_subscriptions_finish ON user_subscriptions(finish)');
|
2025-12-13 13:27:05 +00:00
|
|
|
|
|
|
|
|
// Tests indexes
|
|
|
|
|
await customStatement('CREATE INDEX IF NOT EXISTS idx_test_questions_test_id ON test_questions(test_id)');
|
|
|
|
|
|
|
|
|
|
// StudySessions indexes
|
|
|
|
|
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)');
|
|
|
|
|
|
|
|
|
|
// PromoCodes indexes
|
|
|
|
|
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)');
|
|
|
|
|
|
|
|
|
|
print('Indexes created successfully');
|
|
|
|
|
}
|
|
|
|
|
}
|