Фоновые процессы
Описание
Кроме реактивной обработки апдейтов Telegram и outbox-событий, бот держит
несколько @Cron-задач (@nestjs/schedule) и один пассивный флоу помощи.
Все задачи, пишущие в общие таблицы, single-flight-безопасны через
RedlockService (см. «Архитектура» — сервис рассчитан на 1
реплику, редлок здесь подстраховка, а не механизм масштабирования).
| Задача | Расписание | Файл |
|---|---|---|
| Outbox-поллинг | EVERY_10_SECONDS | src/outbox/outbox-consumer.service.ts |
| Напоминания застрявшим | EVERY_HOUR | src/reminders/reminders.service.ts |
| Дайджест лидеру | EVERY_DAY_AT_NOON | src/notifications/digest.service.ts |
| Отправител ь рассылок (sweep) | EVERY_MINUTE | src/broadcast/broadcast-sender.service.ts |
| Чистка дедуп-леджера outbox | EVERY_DAY_AT_MIDNIGHT | src/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):
- Выбирает
bot_users, застрявших ровно наstage, неактивных дольше 24ч (lastActivityAt < now - 24h) и не отписавшихся от срочных уведомлений (optinUrgent === true). - Для каждого — проверяет
bot_reminders_sentпо(botUserId, reminderKey); если запись уже есть, пропускает (не шлёт дважды). - Берёт текст контент-блока (
ContentService.getBlock(reminderKey, lang)) и шлёт DM (NotifierService.sendMessage). - Вставляет
(botUserId, reminderKey)вbot_reminders_sent(UNIQUE-индекс) — гонка параллельных проходов (не должна случиться при 1 реплике, но защита есть) ловится как дубликат вставки и тихо пропускается.
Идемпотентность — на уровне БД (UNIQUE(bot_user_id, reminder_key)), а не
только in-memory проверки, поэтому напоминание гарантированно не уходит
дважды даже при повторном срабатывании крона в ту же минуту.
Дайджест лидеру (DigestService)
sendDailyDigests() (@Cron(EVERY_DAY_AT_NOON)):
- Собирает уникальные
leaderUidсреди всехbot_usersс непустымleaderUid(first-touch-привязка к лидеру). - Для каждого
leaderUidищет карточку лидера (bot_usersсmagnetUid = leaderUid) — т.е. лидер должен сам быть привязан в боте. - Пропускает, если у лидера нет
telegramIdилиoptinDigest === false. - Иначе строит текст из
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):
- Ставит
bot_users.helpRequested = trueдля нажавшего — это сразу заводит его вgetHotLeads(leaderUid)лидера структуры (см. «🔥 Горячие» в кабинете/списке партнёров). - Если у лида определён лидер структуры (
magnetParentUid ?? leaderUid) и тот привязан в боте и не отключилoptinUrgent— шлёт ему DM «🆘 Ваш партнёр «имя» запросил помощь. Загляните в 👥 Партнёры.». - Нет карточки лида или лидер не найден/отключил уведомления — тихий
пропуск DM, лид всё равно помечен
helpRequested(мягкая деградация, без исключений).
Чистка дедуп-леджера (ProcessedPruneService)
Описана подробно в «Outbox-контракт»:
ежедневно (EVERY_DAY_AT_MIDNIGHT) удаляет строки bot_processed_events
старше 30 дней, чтобы дедуп-леджер обработанных outbox-событий не рос
бесконечно.