Перейти к основному содержимому

Фоновые процессы

Описание

Кроме реактивной обработки апдейтов Telegram и outbox-событий, бот держит несколько @Cron-задач (@nestjs/schedule) и один пассивный флоу помощи. Все задачи, пишущие в общие таблицы, single-flight-безопасны через RedlockService (см. «Архитектура» — сервис рассчитан на 1 реплику, редлок здесь подстраховка, а не механизм масштабирования).

ЗадачаРасписаниеФайл
Outbox-поллингEVERY_10_SECONDSsrc/outbox/outbox-consumer.service.ts
Напоминания застрявшимEVERY_HOURsrc/reminders/reminders.service.ts
Дайджест лидеруEVERY_DAY_AT_NOONsrc/notifications/digest.service.ts
Отправитель рассылок (sweep)EVERY_MINUTEsrc/broadcast/broadcast-sender.service.ts
Чистка дедуп-леджера outboxEVERY_DAY_AT_MIDNIGHTsrc/outbox/processed-prune.service.ts

Outbox-поллинг и отправитель рассылок описаны в «Outbox-контракт» и «Рассылки» соответственно. Ниже — напоминания, дайджест и help-флоу.

Напоминания застрявшим (RemindersService)

sweep() (@Cron(EVERY_HOUR)) проходит по карте REMINDER_BY_STAGE (src/funnel/content.seed.ts, этап воронки → ключ контент-блока напоминания) и для каждой пары вызывает sendRemindersForStage(stage, reminderKey, thresholdHours = 24):

  1. Выбирает bot_users, застрявших ровно на stage, неактивных дольше 24ч (lastActivityAt < now - 24h) и не отписавшихся от срочных уведомлений (optinUrgent === true).
  2. Для каждого — проверяет bot_reminders_sent по (botUserId, reminderKey); если запись уже есть, пропускает (не шлёт дважды).
  3. Берёт текст контент-блока (ContentService.getBlock(reminderKey, lang)) и шлёт DM (NotifierService.sendMessage).
  4. Вставляет (botUserId, reminderKey) в bot_reminders_sent (UNIQUE-индекс) — гонка параллельных проходов (не должна случиться при 1 реплике, но защита есть) ловится как дубликат вставки и тихо пропускается.

Идемпотентность — на уровне БД (UNIQUE(bot_user_id, reminder_key)), а не только in-memory проверки, поэтому напоминание гарантированно не уходит дважды даже при повторном срабатывании крона в ту же минуту.

Дайджест лидеру (DigestService)

sendDailyDigests() (@Cron(EVERY_DAY_AT_NOON)):

  1. Собирает уникальные leaderUid среди всех bot_users с непустым leaderUid (first-touch-привязка к лидеру).
  2. Для каждого leaderUid ищет карточку лидера (bot_users с magnetUid = leaderUid) — т.е. лидер должен сам быть привязан в боте.
  3. Пропускает, если у лидера нет telegramId или optinDigest === false.
  4. Иначе строит текст из LeaderCabinetService.getMetrics(leaderUid) («📊 Дайджест: в боте N, посмотрели M, зарегались K, активировали NFT L») и шлёт DM. Ошибка одного лидера (try/catch внутри цикла) не прерывает рассылку дайджестов остальным.

Help-флоу (m:help_ask)

Не cron, а обработчик кнопки «❓ Задать вопрос» на экране m:help (MenuUpdate.onHelpAsk, apps/onboarding-bot/src/bot/menu.update.ts):

  1. Ставит bot_users.helpRequested = true для нажавшего — это сразу заводит его в getHotLeads(leaderUid) лидера структуры (см. «🔥 Горячие» в кабинете/списке партнёров).
  2. Если у лида определён лидер структуры (magnetParentUid ?? leaderUid) и тот привязан в боте и не отключил optinUrgent — шлёт ему DM «🆘 Ваш партнёр «имя» запросил помощь. Загляните в 👥 Партнёры.».
  3. Нет карточки лида или лидер не найден/отключил уведомления — тихий пропуск DM, лид всё равно помечен helpRequested (мягкая деградация, без исключений).

Чистка дедуп-леджера (ProcessedPruneService)

Описана подробно в «Outbox-контракт»: ежедневно (EVERY_DAY_AT_MIDNIGHT) удаляет строки bot_processed_events старше 30 дней, чтобы дедуп-леджер обработанных outbox-событий не рос бесконечно.