import 'dart:developer'; import 'dart:math' as math; import 'package:drift/drift.dart' as drift; import 'package:mnemo_cards_backend/cron/task.dart' as task; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/database/database.dart' as drift; class GeneratePromocodes with task.Task { final AppDatabase _db; final random = math.Random.secure(); GeneratePromocodes(this._db); @override String get name => 'generate_promocodes'; @override Future task() async { final campaigns = await _db.promoCodeDao.getCampaignsByStatuses([ 'created', 'preparing', ]); await Future.wait([ for (final campaign in campaigns) _generateCampaignPromoCodes(campaign), ]); } static final _letters = 'CDFGJLNQRSUVWXYZ'; static final _digits = '123456789'; static final _symbols = _letters + _digits; String get randomSymbol => _symbols[random.nextInt(_symbols.length)]; String get randomLetter => _letters[random.nextInt(_letters.length)]; String get randomDigit => _digits[random.nextInt(_digits.length)]; String _generateCode(String template) => template .replaceAllMapped(r'$d', (_) => randomDigit) .replaceAllMapped(r'$l', (_) => randomLetter) .replaceAllMapped(r'$s', (_) => randomSymbol); Future _generateCampaignPromoCodes(PromoCodesCampaign campaign) async { print('Generating promo codes for campaign ${campaign.id}'); try { if (campaign.status != 'preparing') { await _db.promoCodeDao.updateCampaignStatus(campaign.id, 'preparing'); } final PAGE_INITIAL = 50; int page = PAGE_INITIAL; while (true) { // Получаем текущее количество промокодов final existingCodes = await _db.promoCodeDao.getPromoCodesByCampaignId( campaign.id, ); final currentCount = existingCodes.length; final leftToGenerate = campaign.generationSize - currentCount; if (leftToGenerate <= 0) { break; } final willGenerate = math.min(leftToGenerate, page); try { await _db.transaction(() async { final promoCodes = []; for (int i = 0; i < willGenerate; i++) { promoCodes.add( drift.PromoCodesCompanion.insert( campaignId: campaign.id, code: _generateCode(campaign.template), activations: const drift.Value(0), ), ); } await _db.promoCodeDao.createPromoCodes(promoCodes); }); if (page < PAGE_INITIAL) { page *= 2; } } catch (e) { // Обработка уникальных нарушений (duplicate key) final errorMessage = e.toString().toLowerCase(); if (page > 1 && (errorMessage.contains('unique') || errorMessage.contains('duplicate') || errorMessage.contains('violates unique constraint'))) { page ~/= 2; print( 'UniqueViolationError when generating codes for ${campaign.id}, retrying with page: $page', ); continue; } else { rethrow; } } } // Проверяем, достигли ли мы нужного количества final finalCodes = await _db.promoCodeDao.getPromoCodesByCampaignId( campaign.id, ); if (finalCodes.length >= campaign.generationSize) { await _db.promoCodeDao.updateCampaignStatus(campaign.id, 'ready'); } print('Finished generating promo codes for campaign ${campaign.id}'); } catch (e, s) { print('Error when generating promo codes for ${campaign.id} $e $s'); log( 'Error when generating promo codes for ${campaign.id}', error: e, stackTrace: s, ); } } @override int get intervalSeconds => 10 * 60; }