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

Onboarding Bot — архитектура

Описание

apps/onboarding-bot — отдельный NestJS-сервис, реализующий Telegram-бота воронки лидов Magnet (nestjs-telegraf, long-polling). Бот ведёт нового лида от /start до регистрации и активации статусного NFT, а после активации превращает Telegram в полноценную кнопочную консоль лидера (метрики структуры, партнёры, рассылки, аналитика воронки).

Сервис физически не может влиять на основной backend напрямую: вся связь — через HTTP-контракт /bot/* под BotAuthGuard (см. «Outbox-контракт»). Собственных бизнес-таблиц backend (users, sessions, ончейн-состояние) бот не касается — только читает их через сервис-эндпоинты.

Компоненты

КомпонентРасположениеНазначение
MagnetApiServicesrc/magnet-api/magnet-api.service.tsHTTP-клиент к /bot/* и /auth/check-account-by-telegram/:id backend'а
OnboardingUpdatesrc/bot/onboarding.update.ts/start, воронка этапов 1–6, first-touch атрибуция лида
BindingServicesrc/bot/binding.service.tsIn-bot подтверждение привязки Telegram↔кошелёк (bind_yes/bind_no)
ViewerServicesrc/bot/viewer.service.tsЕдиный резолвер состояния посетителя (A/B/C) + гейт консоли лидера
MenuUpdatesrc/bot/menu.update.tsКнопочное меню, кабинет, партнёры, воронка, ссылка, уведомления, помощь
LeaderCabinetService / LeaderCabinetUpdatesrc/leader-cabinet/Метрики структуры лидера (getMetrics, getHotLeads, conversionForLeader), команда /cabinet
AdminService / AdminUpdatesrc/admin/Скоупная (segmentForLeader) и глобальная (segment, /admin_*) сегментация
BroadcastUpdatesrc/broadcast/broadcast.update.tsПошаговый мастер рассылки (bc:*)
BroadcastSenderServicesrc/broadcast/broadcast-sender.service.tsФактическая отправка рассылки + @Cron-подборщик запланированных/зависших
RemindersServicesrc/reminders/reminders.service.tsCron-напоминания застрявшим на этапах воронки
DigestServicesrc/notifications/digest.service.tsЕжедневный дайджест лидеру по его структуре
EventHandlersService / OutboxConsumerServicesrc/outbox/Консьюмер доменных событий кабинета (см. outbox-контракт)
ProcessedPruneServicesrc/outbox/processed-prune.service.tsЕжедневная чистка дедуп-леджера обработанных событий
FunnelServicesrc/funnel/funnel.service.tsМонотонное продвижение funnel_stage + запись дискретных фактов
ContentService / content.seed.tssrc/funnel/Контент-блоки воронки (моки, идемпотентный seed при бутстрапе)
NotifierServicesrc/bot/notifier.service.tsТонкая обёртка отправки DM: 403→false, 429→ретрай по retry_after

Коннекшены и данные

Бот держит собственное TypeORM-подключение с именем 'onboarding' (отдельное от подключения основного backend, хотя физически может смотреть на ту же MySQL) — см. src/app.module.ts:

apps/onboarding-bot/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_usersBotUserCRM-карточка лида/партнёра: telegram_id (BIGINT→string), first-touch leaderLink/leaderUid, фактический magnetParentUid, funnelStage, opt-in флаги
bot_funnel_eventsBotFunnelEventЛог дискретных фактов воронки (stage:<N>, click_register, interest:<x>) — источник счётчиков кабинета, НЕ монотонный funnel_stage
bot_content_blocksBotContentBlockКонтент воронки (текст/медиа/кнопки), уникален по (key, lang), сидится идемпотентно при старте
bot_processed_eventsBotProcessedEventДедуп-леджер обработанных outbox-событий (UNIQUE(outboxEventId)), чистится ежедневно
bot_reminders_sentBotReminderSentИдемпотентность напоминаний (UNIQUE(botUserId, reminderKey))
bot_broadcastsBotBroadcastРассылки: сегмент-фильтр, текст/медиа, статус, ownerUid (скоуп структуры), lockedUntil (лиз крон-отправителя)
bot_broadcast_recipientsBotBroadcastRecipientПер-получательный леджер доставки (UNIQUE(broadcastId, botUserId)) — resume/дедуп
bot_bonus_grantsBotBonusGrantНе задействовано. Сущность и BonusService существуют, но провайдер не зарегистрирован в AppModule (осознанно отключено на этапе доработки — см. known limitations)

Таблица backend (владелец — apps/backend)

ТаблицаСущностьНазначение
bot_event_outboxBotEventOutboxТранзакционный outbox доменных событий кабинета, пишется backend'ом, читается ботом через /bot/events/pending + /bot/events/ack

Подробности контракта — в «Outbox-контракт».

Развёртывание и ограничения инстанса

Сервис рассчитан строго на 1 реплику:

  • Long-polling Telegram API (nestjs-telegraf без webhook) — при двух инстансах оба будут получать одни и те же апдейты и дублировать ответы.
  • Мастер рассылки и кэш Viewer живут в ctx.session — сессия telegraf in-memory (per-process); второй инстанс не увидит состояние мастера первого.
  • Все фоновые @Cron-задачи, которые пишут в общие таблицы (outbox-поллинг, crон-отправитель рассылок), защищены RedlockService (single-flight на ключ onboarding:outbox / onboarding:broadcast) — это подстраховка от случайного второго инстанса/деплой-оверлапа, а не способ горизонтального масштабирования.

Бутстрап (src/main.ts) на старте:

  1. Идемпотентно сидит моки контент-блоков воронки (ContentService.seed()).
  2. Регистрирует кнопочные входы Telegram (setMyCommands + синяя menu-button) — обёрнуто в try/catch, недоступность Telegram API на старте не валит бутстрап.
  3. Поднимает HTTP-порт (PORT, по умолчанию 3200) — используется только для health-check, весь пользовательский трафик идёт через long-polling.

Связанные документы