Lead Routing Service — это микросервис на FastAPI, отвечающий за:
- управление операторами и их нагрузкой;
- хранение лидов и их обращений;
- маршрутизацию новых обращений по операторам с учётом лимитов и весов;
- настройку распределения трафика по источникам (ботам).
Проект использует Python, FastAPI, SQLAlchemy, SQLite.
Запуск осуществляется через файл main.py, содержащий конструкцию:
if __name__ == "__main__":
...project/ - корневая директория проекта
├── main.py - точка запуска приложения
├── settings.py - модуль для работы с переменными окружениям
├── .env - файл с секретами (добавлен в VCS специально в демонстрационных целях)
└── .database.db - файл БД (добавлен в VCS специально в демонстрационных целях)
└── requirements.txt - файл с зависимостями
└── app/ - пакет с составляющими сервиса
├── __init__.py - модуль инициализации (предусмотрен для возможного горизонтальеного масштибирования)
├── models.py - модуль с Pydantic моделями
├── schemas.py - модуль со схемой БД
├── services.py - модуль с основной бизнес-логикой сервиса
├── repository.py - модуль для взаимодействия с БД
├── controllers.py - модуль с FastAPI эндпоинтами
├── routers.py - модуль с маршрутизатором сервиса (предусмотрен для возможного горизонтальеного масштибирования)
- Python 3.12+
- FastAPI
- SQLAlchemy (async + sync)
- SQLite
- Pydantic / pydantic-settings
pip install -r requirements.txtПример:
DATABASE_URL=sqlite+aiosqlite:///./database.db
python main.pyFastAPI поднимет сервер на:
http://127.0.0.1:8000
Автоматически создаётся по адресу:
http://127.0.0.1:8000/docs
- name - имя
- is_active - активность
- max_concurrent - лимит нагрузки
- source_weights - relationship атрибут (связь с OperatorSourceWeight)
- contacts - relationship атрибут (связь с Contact)
- code - уникальный идентификатор источника, например bot_telegram
- name - имя
- description - описание источника
- source_weights - relationship атрибут (связь с OperatorSourceWeight)
- contacts - relationship атрибут (связь с Contact)
- operator_id - идентификатор оператора (Foreign Key для Operator.id)
- source_id - идентификатор источника (Foreign Key для Source.id)
- weight - вес / сила связи оператора с источником
- external_id - идентификатор источника, из которого пришел лид. По сути Lead.external_id = Source.code. Необходим для осуществления бизнес логики
- phone - номер телефона
- email - электронная почта
- created_at - дата создания
- contacts - relationship атрибут (связь с Contact)
- lead_id - идентификатор лида (Foreign Key для Lead.id)
- source_id - идентификатор источника (Foreign Key для Source.id)
- operator_id - назначенный оператор или None ((Foreign Key для Operator.id))
- status - статус обращения (Enum-перечисление из new, assigned, in_progress, closed)
- payload - содержимое обращения
- created_at - дата создания
- lead - relationship атрибут (связь с Lead)
- source - relationship атрибут (связь с Source)
- operator - relationship атрибут (связь с Operator)
При поступлении нового обращения система выполняет поиск лида по одному или нескольким уникальным признакам:
- телефон;
- email;
- внешний идентификатор (например, ID пользователя в боте);
Если лид найден — новое обращение связывается с ним. Если нет — создаётся новый лид.
У каждого источника (бота) задан список операторов с их весами:
оператор A — вес 10
оператор B — вес 30
оператор C — вес 60
Для распределения используется формула вероятности:
выбор оператора = вес оператора / сумма всех весов
Пример: Сумма = 10 + 30 + 60 = 100.
- A получает ~10% обращений,
- B — ~30%,
- C — ~60%.
Мы используем взвешенный случайный выбор.
У каждого оператора есть поле max_concurrent — максимально допустимое количество активных обращений.
Перед распределением система фильтрует операторов:
- оператор должен быть активен (
is_active=True), - его текущая нагрузка <
max_concurrent.
Если оператор выбран весовым алгоритмом, но его лимит превышен, система:
- исключает его из списка,
- повторяет выбор среди оставшихся,
- если доступных не остаётся — назначение не происходит.
Если:
- все операторы неактивны,
- или превышены лимиты нагрузки,
- или для источника нет настроенных операторов,
то создаётся обращение без оператора (operator_id = None).
Этот вариант был выбран как наиболее гибкий и безопасный. Ошибка 4xx не возвращается — данные не теряются, обращение фиксируется.
POST /operators
GET /operators
PATCH /operators/{operator_id}
POST /sources
POST /sources/{source_id}
POST /contacts/{source_code}
GET /contacts_and_leads
GET /contacts_by_operators
GET /contacts_by_sources
- Автоматическое создание таблиц происходит при запуске сервиса.
- Лимит нагрузки считается количеством активных обращений оператора.
- Если нет доступного оператора, обращение создаётся без назначения.
