Onboarding Bot — архитектура
Описание
apps/onboarding-bot — отдельный NestJS-сервис, реализующий Telegram-бота
воронки лидов Magnet (nestjs-telegraf, long-polling). Бот ведёт нового лида
от /start до регистрации и активации статусного NFT, а после активации
превращает Telegram в полноценную кнопочную консоль лидера (метрики
структуры, партнёры, рассылки, аналитика воронки).
Сервис физически не может влиять на основной backend напрямую: вся связь —
через HTTP-контракт /bot/* под BotAuthGuard (см.
«Outbox-контракт»). Собственных бизнес-таблиц backend
(users, sessions, ончейн-состояние) бот не касается — только читает их
через сервис-эндпоинты.
Компоненты
| Компонент | Расположение | Назначение |
|---|---|---|
MagnetApiService | src/magnet-api/magnet-api.service.ts | HTTP-клиент к /bot/* и /auth/check-account-by-telegram/:id backend'а |
OnboardingUpdate | src/bot/onboarding.update.ts | /start, воронка этапов 1–6, first-touch атрибуция лида |
BindingService | src/bot/binding.service.ts | In-bot подтверждение привязки Telegram↔кошелёк (bind_yes/bind_no) |
ViewerService | src/bot/viewer.service.ts | Единый резолвер состояния посетителя (A/B/C) + гейт консоли лидера |
MenuUpdate | src/bot/menu.update.ts | Кнопочное меню, кабинет, партнёры, воронка, ссылка, уведомления, помощь |
LeaderCabinetService / LeaderCabinetUpdate | src/leader-cabinet/ | Метрики структуры лидера (getMetrics, getHotLeads, conversionForLeader), команда /cabinet |
AdminService / AdminUpdate | src/admin/ | Скоупная (segmentForLeader) и глобальная (segment, /admin_*) сегментация |
BroadcastUpdate | src/broadcast/broadcast.update.ts | Пошаговый мастер рассылки (bc:*) |
BroadcastSenderService | src/broadcast/broadcast-sender.service.ts | Фактическая отправка рассылки + @Cron-подборщик запланированных/зависших |
RemindersService | src/reminders/reminders.service.ts | Cron-напоминания застрявшим на этапах воронки |
DigestService | src/notifications/digest.service.ts | Ежедневный дайджест лидеру по его структуре |
EventHandlersService / OutboxConsumerService | src/outbox/ | Консьюмер доменных событий кабинета (см. outbox-контракт) |
ProcessedPruneService | src/outbox/processed-prune.service.ts | Ежедневная чистка дедуп-леджера обработанных событий |
FunnelService | src/funnel/funnel.service.ts | Монотонное продвижение funnel_stage + запись дискретных фактов |
ContentService / content.seed.ts | src/funnel/ | Контент-блоки воронки (моки, идемпотентный seed при бутстрапе) |
NotifierService | src/bot/notifier.service.ts | Тонкая обёртка отправки DM: 403→false, 429→ретрай по retry_after |
Коннекшены и данные
Бот держит собственное TypeORM-подключение с именем 'onboarding'
(отдельное от подключения основного backend, хотя физически может смотреть на
ту же MySQL) — см. src/app.module.ts:
TypeOrmModule.forRootAsync({
name: 'onboarding',
useFactory: (config: ConfigService) => ({
name: 'onboarding',
type: 'mysql',
// ...
synchronize: false,
migrations: ONBOARDING_MIGRATIONS,
migrationsRun: true,
migrationsTableName: 'onboarding_migrations',
namingStrategy: new SnakeNamingStrategy(),
extra: {
enableKeepAlive: true,
keepAliveInitialDelay: 10000,
},
}),
});
migrationsRun: true— миграции бота прогоняются автоматически на старте (списокONBOARDING_MIGRATIONSвsrc/data-source.ts, тот же массив используется и CLI (typeorm migration:run), и рантайм-подключением — они никогда не расходятся).migrationsTableName: 'onboarding_migrations'— отдельная таблица учёта миграций, не пересекается с миграциями основного backend.enableKeepAlive— бот подолгу простаивает между сообщениями и ходит в MySQL через docker/swarm overlay-сеть, где NAT/conntrack рвёт неактивные TCP-соединения; TCP keep-alive держит пул живым и не даёт словитьread ECONNRESETна следующем запросе.- Инъекции репозиториев везде указывают второй параметр
'onboarding'(@InjectRepository(BotUser, 'onboarding')) — критично при работе рядом с основным backend-подключением в том же процессе (если когда-либо объединятся).
Таблицы бота (bot_*, владелец — бот)
| Таблица | Сущность | Назначение |
|---|---|---|
bot_users | BotUser | CRM-карточка лида/партнёра: telegram_id (BIGINT→string), first-touch leaderLink/leaderUid, фактический magnetParentUid, funnelStage, opt-in флаги |
bot_funnel_events | BotFunnelEvent | Лог дискретных фактов воронки (stage:<N>, click_register, interest:<x>) — источник счётчиков кабинета, НЕ монотонный funnel_stage |
bot_content_blocks | BotContentBlock | Контент воронки (текст/медиа/кнопки), уникален по (key, lang), сидится идемпотентно при старте |
bot_processed_events | BotProcessedEvent | Дедуп-леджер обработанных outbox-событий (UNIQUE(outboxEventId)), чистится ежедневно |
bot_reminders_sent | BotReminderSent | Идемпотентность напоминаний (UNIQUE(botUserId, reminderKey)) |
bot_broadcasts | BotBroadcast | Рассылки: сегмент-фильтр, текст/медиа, статус, ownerUid (скоуп структуры), lockedUntil (лиз крон-отправителя) |
bot_broadcast_recipients | BotBroadcastRecipient | Пер-получательный леджер доставки (UNIQUE(broadcastId, botUserId)) — resume/дедуп |
bot_bonus_grants | BotBonusGrant | Не задействовано. Сущность и BonusService существуют, но провайдер не зарегистрирован в AppModule (осознанно отключено на этапе доработки — см. known limitations) |
Таблица backend (владелец — apps/backend)
| Таблица | Сущность | Назначение |
|---|---|---|
bot_event_outbox | BotEventOutbox | Транзакционный outbox доменных событий кабинета, пишется backend'ом, читается ботом через /bot/events/pending + /bot/events/ack |
Подробности контракта — в «Outbox-контракт».
Развёртывание и ограничения инстанса
Сервис рассчитан строго на 1 реплику:
- Long-polling Telegram API (
nestjs-telegrafбез webhook) — при двух инстансах оба будут получать одни и те же апдейты и дублировать ответы. - Мастер рассылки и кэш
Viewerживут вctx.session— сессия telegrafin-memory(per-process); второй инстанс не увидит состояние мастера первого. - Все фоновые
@Cron-задачи, которые пишут в общи е таблицы (outbox-поллинг, crон-отправитель рассылок), защищеныRedlockService(single-flight на ключonboarding:outbox/onboarding:broadcast) — это подстраховка от случайного второго инстанса/деплой-оверлапа, а не способ горизонтального масштабирования.
Бутстрап (src/main.ts) на старте:
- Идемпотентно сидит моки контент-блоков воронки (
ContentService.seed()). - Регистрирует кнопочные входы Telegram (
setMyCommands+ синяя menu-button) — обёрнуто вtry/catch, недоступность Telegram API на старте не валит бутстрап. - Поднимает HTTP-порт (
PORT, по умолчанию 3200) — используется только для health-check, весь пользовательский трафик идёт через long-polling.
Связанные документы
- Outbox-контракт — доменные события, at-least-once, dead-letter.
- Меню-навигация — кнопочная консоль лидера, состояния A/B/C, гейтинг.
- Рассылки — мастер + отправитель + гарантии доставки.
- Фоновые процессы — напоминания, дайджест, help-флоу.
- Известные ограничения — что осознанно отложено и почему.