diff --git a/mnemo_cards_backend/lib/cron/tasks_seeder.dart b/mnemo_cards_backend/lib/cron/tasks_seeder.dart index c4234b4..38515de 100644 --- a/mnemo_cards_backend/lib/cron/tasks_seeder.dart +++ b/mnemo_cards_backend/lib/cron/tasks_seeder.dart @@ -219,35 +219,41 @@ class TasksSeederTask with cron_task.Task { ), ]; - // Convert UserTaskModel to UserTasksCompanion and insert - // PostgreSQL автоматически генерирует UUID через DEFAULT gen_random_uuid() - // Используем InsertMode.insertOrIgnore с предопределенными UUID для идемпотентности + // Convert UserTaskModel to UserTasksCompanion and insert через DAO + // Используем предопределенные UUID для идемпотентности seeder'а + int insertedCount = 0; + int skippedCount = 0; + for (final taskData in tasksWithIds) { final taskModel = taskData.task; final rewardsJson = taskModel.rewards.map((r) => r.toJson()).toList(); - await _db.into(_db.userTasks).insert( - UserTasksCompanion.insert( - id: Value(taskData.id), // Предопределенный UUID для идемпотентности seeder'а - title: taskModel.title, - description: taskModel.description, - type: taskModel.type, - difficulty: taskModel.difficulty, - status: taskModel.status, - rewards: Value(rewardsJson), - createdAt: Value(PgDateTime(taskModel.createdAt)), - expiresAt: PgDateTime(taskModel.expiresAt), - completedAt: Value(taskModel.completedAt?.let(PgDateTime.new)), - proofUrl: Value(taskModel.proofUrl), - instructions: Value(taskModel.instructions), - tags: Value(taskModel.tags), - imageUrl: Value(taskModel.imageUrl), - ), - mode: InsertMode.insertOrIgnore, // ON CONFLICT DO NOTHING + final companion = UserTasksCompanion.insert( + id: Value(taskData.id), // Предопределенный UUID для идемпотентности seeder'а + title: taskModel.title, + description: taskModel.description, + type: taskModel.type, + difficulty: taskModel.difficulty, + status: taskModel.status, + rewards: Value(rewardsJson), + createdAt: Value(PgDateTime(taskModel.createdAt)), + expiresAt: PgDateTime(taskModel.expiresAt), + completedAt: Value(taskModel.completedAt?.let(PgDateTime.new)), + proofUrl: Value(taskModel.proofUrl), + instructions: Value(taskModel.instructions), + tags: Value(taskModel.tags), + imageUrl: Value(taskModel.imageUrl), ); + + final wasCreated = await _db.taskDao.createUserTaskOrIgnore(companion); + if (wasCreated) { + insertedCount++; + } else { + skippedCount++; + } } - print('Successfully seeded ${tasksWithIds.length} user tasks (duplicates ignored)'); + print('Successfully seeded user tasks: $insertedCount inserted, $skippedCount skipped (already exist)'); } catch (e) { print('Error seeding tasks: $e'); rethrow; diff --git a/mnemo_cards_backend/lib/database/daos/task_dao.dart b/mnemo_cards_backend/lib/database/daos/task_dao.dart index e5f679d..3087ff5 100644 --- a/mnemo_cards_backend/lib/database/daos/task_dao.dart +++ b/mnemo_cards_backend/lib/database/daos/task_dao.dart @@ -82,6 +82,39 @@ class TaskDao extends DatabaseAccessor with _$TaskDaoMixin { final inserted = await into(db.userTasks).insertReturning(task); return inserted.id; } + + /// Создать задачу пользователя или игнорировать, если уже существует + /// Возвращает true, если задача была создана, false если уже существовала + /// + /// Если task.id.present == true, проверяет существование перед вставкой + Future createUserTaskOrIgnore(UserTasksCompanion task) async { + // Если ID указан, проверяем существование перед вставкой + if (task.id.present) { + final existing = await getUserTaskById(task.id.value); + if (existing != null) { + return false; // Задача уже существует + } + } + + try { + await into(db.userTasks).insert( + task, + mode: InsertMode.insertOrIgnore, + ); + return true; // Задача была создана + } catch (e) { + // PostgreSQL error code 23505 = unique_violation (duplicate key) + // Это может произойти в редких случаях race condition + final errorString = e.toString(); + if (errorString.contains('23505') || + errorString.contains('duplicate key') || + errorString.contains('unique constraint')) { + return false; // Задача уже существует + } + // Пробрасываем другие ошибки + rethrow; + } + } /// Обновить задачу пользователя Future updateUserTask(UserTask task) { diff --git a/mnemo_cards_backend/lib/database/database.dart b/mnemo_cards_backend/lib/database/database.dart index db964da..c387660 100644 --- a/mnemo_cards_backend/lib/database/database.dart +++ b/mnemo_cards_backend/lib/database/database.dart @@ -179,14 +179,6 @@ class AppDatabase extends _$AppDatabase { final result = await customSelect('SELECT 1 as test').getSingle(); print('Database connection successful: ${result.data}'); - // Удалить constraint valid_email если он существует - try { - await customStatement('ALTER TABLE users DROP CONSTRAINT IF EXISTS valid_email'); - print('Removed valid_email constraint if it existed'); - } catch (e) { - print('Warning: Could not remove valid_email constraint: $e'); - } - // Включить foreign key constraints await customStatement('SET CONSTRAINTS ALL IMMEDIATE'); }, diff --git a/mnemo_cards_backend/lib/user/user_manager.dart b/mnemo_cards_backend/lib/user/user_manager.dart index 6a0ecbb..702b4a4 100644 --- a/mnemo_cards_backend/lib/user/user_manager.dart +++ b/mnemo_cards_backend/lib/user/user_manager.dart @@ -125,21 +125,28 @@ class UserManager { print('Creating new user $name $email'); - // Create new user + // Create new user with user data in transaction + final now = DateTime.now(); final userCompanion = UsersCompanion.insert( externalUserId: externalId, name: drift.Value(name), email: drift.Value(email), + // Explicitly set fields with default values to ensure they're returned by insertReturning + admin: drift.Value(false), + purchases: drift.Value([]), + createdAt: drift.Value(PgDateTime(now)), + updatedAt: drift.Value(PgDateTime(now)), + isDeleted: drift.Value(false), ); - final userId = await _db.userDao.createUser(userCompanion); + final userDataCompanion = UserDatasCompanion.insert( + userId: '', // Will be set by createUserWithData via copyWith + registrationDate: drift.Value(PgDateTime(now)), + ); - // Create user data - await _db.userDao.createUserData( - UserDatasCompanion.insert( - userId: userId, - registrationDate: drift.Value(PgDateTime(DateTime.now())), - ), + final userId = await _db.userDao.createUserWithData( + user: userCompanion, + userData: userDataCompanion, ); final user = await _db.userDao.getUserById(userId);