Внутренние сервисы
SMS-шлюз: beeline-service
Для разработчиков. Состояние на 28.09.2026. Переменные, все ручки и выкатка по шагам — README репозитория Buharestskie/beeline-service. Как лендинги работают целиком — статья о квиз-лендингах в этой же книге.
Коротко
beeline-service — общий шлюз исходящих SMS. Сервис присылает номер и готовый текст, шлюз проверяет, что номер российский мобильный, применяет антиспам-лимиты и отдаёт сообщение Билайну (A2P) или хабу МегаФона. Шаблонов у шлюза нет: текст целиком приходит от вызывающего. Снаружи шлюз доступен по адресу https://donula26.ru/service/beeline, пускает только адреса из своего списка доступа.
Кто вызывает
Сейчас — три квиз-лендинга, которые подтверждают телефон посетителя кодом из SMS («Ваш код подтверждения: 1234»):
| Лендинг | Что передаёт |
|---|---|
do-nula-refinancing-2 | phone, message, ip посетителя. lead_id не передаёт никто |
bfl_fspp | |
mfcspisan (на стороннем сервере) |
mfctest шлюз не вызывает: SMS-подтверждения у него нет, после телефона посетитель уходит в мессенджер. Все вызывающие ходят через nginx по публичному адресу, поэтому адрес каждого нового вызывающего нужно заранее внести в ALLOWED_CALLER_CIDRS, иначе он получит 403 Caller IP is not permitted.
Как устроен
- Один процесс FastAPI (Python 3.12, httpx, APScheduler) и свой Redis в том же compose. Redis нужен только для лимитов. Базы нет, тестов нет.
- Номер приводится к
+7XXXXXXXXXX. Принимаются+7…,8…и десять цифр с пробелами, скобками и дефисами. Всё, что не складывается в десять цифр с первой девяткой, получает400«Введите правильный российский номер» — до лимитов и до провайдера. - Четыре лимита проверяются по порядку, каждый отвечает
429с заголовкомRetry-After:Лимит Порог Когда действует Повтор на тот же номер 1 SMS в 60 секунд только если передан ipРазных номеров с одного ip3 за 24 часа только если передан ipSMS на один номер 3 за 24 часа всегда Всего SMS 1000 за сутки по UTC всегда ip— адрес конечного посетителя, который видел бэкенд лендинга, а не адрес самого лендинга. Пороги — константы вapp/constants.py, меняются только вместе с кодом. - Отправитель в обоих каналах —
donulya.ru. Вызывающий получает202и сырой ответ провайдера строкой вprovider_response. Ошибка провайдера —502. - Примечание в amoCRM. Если в запросе есть
lead_id, после ответа фоновой задачей в сделку добавляется примечание «исходящая SMS» с номером и текстом. Шлюз пишет его своим токеном напрямую, не черезamocrm-serviceи не черезamo_queue, со своим ограничением 6 запросов в секунду. Ошибка примечания только пишется в лог.
Билайн или МегаФон
SMS уходит в хаб МегаФона, только если выполнены оба условия:
- на странице настроек шлюза включён переключатель «Использовать сервис Мегафон»;
- номер по плану нумерации выделен МегаФону: таблица диапазонов лежит в
app/services/megafon_ranges.json(25 DEF-кодов) и читается один раз при старте.
Всё остальное идёт через Билайн. По умолчанию переключатель выключен, то есть без ручного включения все SMS уходят Билайну.
- Страница настроек —
https://donula26.ru/service/beeline/megafon-sms-settings: переключатель и кнопка «Сохранить». Нужны одновременно адрес из списка доступа и отдельный логин и пароль страницы (MEGAFON_SETTINGS_PAGE_LOGIN/MEGAFON_SETTINGS_PAGE_PASSWORD, к учётке МегаФона отношения не имеют). - Первого числа каждого месяца в 00:00 по Москве переключатель выключается сам. Если в этот момент сервис лежал, выключение сработает при старте в тот же день.
- Оператор определяется без учёта переноса номера. Номер, ушедший от МегаФона к другому оператору, всё равно пойдёт через МегаФон, и наоборот. Скрипта для обновления диапазонов в репозитории нет.
Как вызвать
curl -X POST https://donula26.ru/service/beeline/sms/send \
-H 'Content-Type: application/json' \
-d '{"phone": "+7 000 000-00-01", "message": "Ваш код подтверждения: 4821", "ip": "<ip посетителя>"}'
| Поле | Обязательно | Что это |
|---|---|---|
phone | да | Российский мобильный в любом написании, 5–32 символа |
message | да | Текст, 1–1000 символов, уходит провайдеру как есть |
ip | нет | Адрес посетителя. Без него не действуют лимит повтора и лимит номеров на IP |
lead_id | нет | Сделка amoCRM для примечания об SMS |
Проверка живости — GET /sms/health, открыта без списка доступа, отвечает {"status":"ok"}.
Где код и как выкатывается
- Репозиторий
Buharestskie/beeline-service, веткаmain. - На сервере — git-клон
main:git pull --ff-onlyиdocker compose up -d --build. Контейнер опубликован только на локальном интерфейсе сервера, снаружи — через nginx с префиксом/service/beeline/. - Внешней пробы в мониторинге у шлюза нет, поэтому окно обслуживания на выкатку объявить не на что.
- Локально:
cp .env.example .env, заполнить, поднять Redis иuvicorn app.main:app --port 8001. С боевыми ключами SMS уйдут на настоящие номера.
На что наступают
- После выкатки переключатель МегаФона снова выключен. Его состояние хранится в файле внутри контейнера, тома под него нет. Если маршрут был включён — перед выкаткой проверьте его (
GET /megafon-sms-settings/api) и после неё включите заново на странице настроек. 202от шлюза не значит, что МегаФон принял SMS. Хаб отвечает HTTP 200 и при отказе, вердикт лежит в теле. Шлюз такой отказ только пишет в лог. Смотрите вprovider_response: у МегаФона тамerror.code,0— принято.- Передавайте номер всегда в одном формате. Лимиты считаются по строке
phoneкак её прислали:+79000000001и8 900 000-00-01— это разные счётчики. - Счётчики растут и на отказах. Если сработал другой лимит или провайдер вернул
502, повторить отправку на тот же номер можно только через 60 секунд, а суточный счётчик номера уже увеличен. - Суточный лимит в 1000 SMS обнуляется в 03:00 по Москве (сутки считаются по UTC), а ежемесячный сброс МегаФона срабатывает в 00:00 по Москве.
- Адрес шлюза в
.env.exampleлендингов неправильный. Уbfl_fsppиmfcspisanтам/services/beelineс «s», уdo-nula-refinancing-2— адрес внутри чужой docker-сети. Рабочий адрес один:https://donula26.ru/service/beeline. - Страница настроек локально не загрузит состояние: её скрипт ходит на боевой путь
/service/beeline/megafon-sms-settings/api. Локально переключайте маршрут черезcurlна/megafon-sms-settings/api. - Примечания с
lead_idнагружают общий лимит amoCRM мимо общего шлюза. Если начнёте передаватьlead_idмассово, учитывайте это вместе с остальными сервисами (см. «Как ходить в amoCRM из своего сервиса»).
Воронка найма: hiring_project
Для разработчиков. Состояние на 28.09.2026. Переменные, все ручки, порядок выкатки и отката — README и RUNBOOK репозитория Buharestskie/hiring_project.
Коротко
hiring_project — воронка найма менеджеров по продажам («охотников») для самой компании: от входа кандидата на сайт до выхода на обучение. Кандидат входит через Telegram, VK или MAX, заполняет анкету и записывает голосовой ответ. Голос оценивает Gemini, решение принимает РОП прямо в Google Sheet, дальше кандидату приходит сообщение в мессенджер, приглашение на групповое собеседование в Телемосте и ссылка на онлайн-обучение. В amoCRM кандидат попадает только после собеседования.
Сейчас сервис на наших серверах не развёрнут: проект передан новому владельцу и ждёт переезда. Домен hiring.donula.online, заложенный в целевую схему, пока не настроен. Внешней пробы в мониторинге нет.
Путь кандидата
- Вход. Сайт запрашивает одноразовый токен (
/api/auth/token, живёт 10 минут) и открывает бота в выбранном мессенджере. Кандидат отправляет боту телефон, сайт опрашивает/api/auth/checkи получает имя и телефон из мессенджера, а не из формы. - Анкета.
/api/submitпринимает анкету и голос (audio/webm,audio/mp4,audio/m4a, от 1 KiB до 10 MiB). Анкета сначала пишется в журнал на диске, затем строкой в лист «Кандидаты» через Apps Script. Повтор с тем жеrequest_idвторую строку не создаёт. - Оценка. Бэкенд отправляет голос в Gemini (через ProxyAPI): пять критериев от 1 до 5. 20 баллов и выше — PASS, кандидату сразу уходит приглашение на собеседование; 10–19 — GRAY, решает РОП; меньше 10 — FAIL, кандидату уходит отказ. Если модель недоступна, ставится GRAY с пометкой «AI недоступен, слушайте вручную».
- Решение РОПа — в таблице: столбец T («Одобрен» / «Отклонён») и W — итог собеседования («Принят» / «Отклонён» / «Не пришел»). Apps Script складывает изменения в лист «Webhook Outbox» и доставляет их в бэкенд подписанным запросом на
/api/webhook/rop; бэкенд пишет кандидату. - Собеседование — ближайший понедельник, среда или пятница не раньше чем через сутки, в 12:00 по Москве, в Телемосте. Напоминания — за 24 часа и за час.
- Обучение. После «Принят» создаются контакт и сделка
Найм: <имя>в воронке найма amoCRM, кандидат получает персональную ссылку на курс (живёт 90 дней). Сайт обучения отмечает визит и прохождение, сделка двигается только вперёд: принят → зашёл на обучение → готов к ролевке. После курса открывается ссылка в закрытое сообщество.
Как устроен
- Бэкенд — один процесс FastAPI на Python 3.12 (
runtime/backend/). Состояние — в каталогеDATA_DIR: SQLite для переписки, JSON-файлы для соответствия чатов, записей на собеседование и токенов amoCRM, и журнал анкет. Журнал одновременно служит очередью эффектов: после рестарта недоделанное доигрывается. - Google Sheet и Apps Script (
runtime/apps-script/google-apps-script-hiring.js) — рабочее место РОПа: листы «Кандидаты», «Обучение», «Трафик», «Расписание МР», «Дашборд», «Системное», «Webhook Outbox». Скрипт выкатывается вручную в редакторе таблицы. - Мессенджеры — своя реализация, не общая с
messenger_connector: Telegram через long polling, VK и MAX через вебхуки. - Inbox (
runtime/sites/inbox) — общая лента переписки со всеми кандидатами из трёх мессенджеров и ответ из веб-интерфейса. - Фоновые циклы внутри процесса: напоминания, вечерняя сводка РОПу в Telegram-группу в 20:00 и утренняя в 09:00 по Москве.
- Статика
runtime/sites/{auth,inbox,training}отдаётся nginx и обращается к бэкенду по относительным/api/…— открывать её нужно с того же адреса, что и бэкенд.
Где код и как запустить
- Репозиторий
Buharestskie/hiring_project. Документы о передаче проекта (STATUS.md,TRANSFER.md,ROUTE_MAP.md) описывают старое размещение и устарели. - Офлайн-тесты (внешняя сеть заблокирована, нужен ещё Node.js для тестов Apps Script):
bash runtime/tests/run_all.shпосле установкиruntime/backend/requirements.lockв venv. - Бэкенд локально —
uvicorn main:appизruntime/backendс явнымиDATA_DIR,DISABLE_SCHEDULED=trueиTG_POLLING_ENABLED=false. Без явногоDATA_DIRзапуск упадёт на правах доступа./api/submitбез настоящего Apps Script тестовой таблицы отвечает503. - Целевая выкатка — релизными архивами:
scripts/build-release.shсобирает архив,scripts/deploy-release.shраскладывает его на хост и переключает симлинк, при провале/healthвозвращает предыдущий;scripts/rollback-release.sh— откат. systemd-юниты и шаблон nginx — вinfra/. Когда сервис будет развёрнут, сначала завести внешнюю пробу, потом выкатывать в окне обслуживания.
На что наступают
- Только один Telegram-поллер и один планировщик. Второй процесс на тех же токенах без
DISABLE_SCHEDULED=trueиTG_POLLING_ENABLED=falseдаёт дубли сообщений кандидатам. DISABLE_SCHEDULEDне выключает эффекты анкеты. Оценка, сообщение кандидату и уведомление РОПу запускаются прямо из/api/submit. Стенд с боевыми токенами пишет живым людям.- Утренняя сводка включена по умолчанию. Если
ENABLE_ROP_MORNING_NOTIFICATIONне задана, она уходит, хотя по решению проекта выключена. - Сессии входа живут в памяти. Рестарт сбрасывает незавершённые входы кандидатов и начатую авторизацию amoCRM. Токен входа удаляется через 10 минут, поэтому напоминания «через 30/60 минут» почти не срабатывают.
- Строки в «Кандидатах» не удалять и не сортировать. Строки находятся по скрытому
request_idв столбце X. Листы «Системное» и «Webhook Outbox» руками не редактируются. - Триггер Apps Script должен быть installable и работает от имени создателя. Потерял аккаунт доступ — решения РОПа копятся в «Webhook Outbox» и не уходят.
- Поток GRAY с пометкой «AI недоступен» — почти всегда кончился ключ или баланс ProxyAPI.
- Сделку в amoCRM бэкенд ищет по названию
Найм: <имя>(первый результат) — однофамильцы могут перепутаться. Ошибка создания сделки только логируется. - MAX:
user_idне равенchat_id, соответствие хранится в JSON-файле вDATA_DIR. Потеря файла ломает доставку в MAX. - Запись голоса во встроенном браузере мессенджера не работает — сайт просит открыть ссылку в Safari или Chrome.
build-release.shне проверяет чистоту дерева (проверка унаследована из монорепозитория и смотрит не туда): незакоммиченные правки попадут в архив под чужим SHA. Перед сборкой —git status.runtime/backend/Dockerfile,backup_sheets.shиlegacy/— наследие старого размещения, целевая схема их не использует.