Контекст
Разрабатывал корпоративную CRM-систему, которая заменила legacy-решение на базе 1С.
Система автоматизировала работу отдела продаж и являлась бизнес-критичной: любые ошибки в данных или простои напрямую влияли на операционную деятельность компании.
Помимо разработки новой функциональности, участвовал в проектировании архитектуры, миграции данных, внедрении системы и сопровождении после запуска.
Изначальный каркас фронтенда создал фронтенд-разработчик, но дальше я полностью дорабатываю и развиваю его самостоятельно.
Архитектура
С самого начала было понятно, что микросервисная архитектура для этого проекта была бы избыточной. Система не предполагала высокой нагрузки, в ней работала небольшая команда, а требования не диктовали необходимость независимого масштабирования или отдельных циклов развёртывания сервисов. Поэтому я выбрал модульный монолит на Django: одна кодовая база, одна база MySQL, а бизнес-логика разделена по приложениям — CRM, авторизация, интеграции и другим модулям.
Фоновые задачи я вынес в Celery, потому что там быстро начали появляться интеграции, отложенные операции, синки с 1С, уведомления и прочие фоновые процессы. Плюс был слой WebSocket для работы в реальном времени. Redis использовался как брокер очередей и как слой для WebSocket.
Если упрощать, CRM состоит из трёх больших блоков:
- Клиенты и продажи — клиенты, лиды, взаимодействия, статусы, товары, сделки.
- Коммуникации — звонки через Mango, уведомления в Telegram и Max, WebSocket-события.
- Интеграции — 1С, сайт, Яндекс.Метрика.
Важный момент: 1С использовалась только как источник товаров. Всё, что касается клиентов и продаж, жило в CRM. Это было принципиально — чтобы не получить двусторонний хаос в данных.
Потоки данных
Данные приходили по трём основным каналам:
Сайт → API → Celery → создание клиента + лида (автоматически)
Менеджер → ручное создание лида в системе
Mango → вебхук звонка → автосоздание/привязка интереса по правилам владения
Ключевое: лиды приходят «ничейными» — они не привязаны к менеджеру в момент создания, а попадают в общий пул и оттуда распределяются (см. следующий раздел). Это осознанное решение: раньше принадлежность назначалась слишком рано и порождала перекосы и «перехваты» клиентов.
Конкурентная обработка данных
Одной из наиболее сложных задач стала реализация операций, которые одновременно выполнялись несколькими пользователями и фоновыми процессами.
Для предотвращения гонок данных активно использовал:
transaction.atomic;- блокировки строк (
select_for_update); transaction.on_commit.
Это позволило гарантировать корректность бизнес-инвариантов даже при высокой степени конкурентного доступа.
Телефония (Mango Office, ВАТС/VPBX)
Двусторонняя интеграция с телефонией — одна из самых проработанных подсистем.
- Исходящие — click-to-call. Менеджер жмёт «Позвонить» в карточке клиента: АТС сначала дозванивается самому менеджеру, он берёт трубку — и только потом соединяется с клиентом. Звонок сразу фиксируется в CRM и попадает в историю клиента.
- Входящие — вебхуки. Mango шлёт события о звонках, из которых система собирает запись разговора, статус и привязку к клиенту.
- «Звонит твой клиент — звонит тебе». На входящем звонке система по номеру находит клиента и, если он закреплён за менеджером, автоматически переводит звонок именно на него.
- Автосоздание обращения из звонка. Входящий звонок превращается в интерес по понятным правилам владения (закреплён за менеджером → на него; никто не взял → в общую очередь распределения). Неизвестный номер заводит нового потенциального клиента.
- Записи разговоров привязываются к клиенту и обращению и доступны прямо в хронологии.
Отдельная сложность — гонки: события по одному звонку приходят несколько раз, поэтому перевод и привязка к обращению защищены так, чтобы на один звонок не создавалось два интереса и звонок не переводился дважды.
Уведомления
Отдельный контур оповещений через Telegram и Max: система реагирует на бизнес-события (новые обращения, значимые изменения по клиентам/сделкам) и доставляет уведомления менеджерам.
Отправка Telegram-уведомлений была реализована через прокси в связи с блокировками.
Вебсокет
Слой WebSocket (daphne + Redis) обеспечивал работу в реальном времени: события распределения и появления новых обращений, уведомления по звонкам Mango, изменения по клиентам и статусам доходили до интерфейса без перезагрузки страницы. Для работы в общем пуле лидов это важно — менеджер видит актуальное состояние, а не устаревший снимок.
Интеграции
- 1С — только источник товаров, односторонний поток. Всё про клиентов и продажи живёт в CRM. Это осознанно исключало двусторонний хаос данных.
- Сайт — заявки с сайта приходят по API и через Celery автоматически создают клиента и лида.
- Яндекс.Метрика — использовалась не для «веб-аналитики ради аналитики», а как сигнал о
поведении клиента, в первую очередь для выявления спама. Это решало сразу две задачи:
- Маркетинговая статистика. Отдел маркетинга получал данные о качестве трафика и источников — какие каналы приводят реальных клиентов, а какие спам.
- Контроль злоупотреблений менеджеров. У менеджеров была мотивация ставить статус «Спам» клиентам, которым просто не смогли продать, — чтобы не портить себе статистику продаж. Данные Метрики позволяли увидеть реальную картину поведения клиента и поймать такое злоупотребление.
Второй пункт — хороший пример того, как интеграция из «аналитики» превратилась в инструмент контроля данных и честности статистики.
Проблема: данные, а не код
Самым сложным была не разработка, а данные и миграция со старой CRM.
В старой 1С-системе была довольно проблемная модель:
- Клиент мог принадлежать нескольким менеджерам, при этом дубли клиентов создавались постоянно и никак не контролировались — хотя изначальное бизнес-правило было обратным: у клиента должен быть один ответственный менеджер.
- Сама модель клиентов была раздвоена: потенциальный и постоянный клиент были разными сущностями. Потенциальный становился постоянным только после первой покупки, и при этом данные не «схлопывались», а продолжали жить параллельно — это создавало дубли.
- При выгрузке не было нормальных идентификаторов связей: «интересы» или «взаимодействия» нельзя было однозначно привязать к клиенту или менеджеру.
В какой-то момент я просто не мог корректно собрать модель у себя и начал обсуждать это с 1С-разработчиками. Мы пришли к тому, что без уникальных идентификаторов на уровне сущностей нормальную миграцию сделать невозможно. Договорились так: они добавляют уникальные ID и нормальную структуру связей, а я на своей стороне строю нормализацию и корректно объединяю постоянных и потенциальных клиентов.
Миграция данных
Даже после этого данные оставались грязными. Например, один и тот же клиент мог быть задублирован несколько раз и при этом иметь разных менеджеров.
Я решил это через бизнес-правило: «последняя продажа определяет основного менеджера». Это было согласовано с руководством, потому что идеальной истины в данных уже не существовало — её пришлось восстановить из поведения системы.
Отдельная сложность — раздвоенная модель клиента: часть обращений висела на потенциальной сущности, а часть — на постоянной, но по факту это был один и тот же человек. Решил это так: если у клиента есть постоянная модель, все интересы потенциальной переносятся на неё. А если дублировалась сама постоянная модель, оставлял только одну сущность — по дате последнего обращения.
Внедрение
- Сначала я дал менеджерам тестовую версию, чтобы они просто попробовали систему и дали обратную связь.
- Затем я записал обучающий вебинар и сделал видео-инструкции — без этого переход был бы очень болезненным.
- После запуска старая CRM ещё какое-то время оставалась как резервная система, но основная работа уже шла в новой.
Ключевые архитектурные решения
- Транзакционность. Активно использовал
atomic, блокировки иon_commit, потому что в системе было много гонок, особенно вокруг распределения лидов и обработки событий телефонии. - Celery как ядро асинхронности. Центральный механизм всей асинхронной логики — от лидов до интеграций.
- Сознательный отказ от микросервисов. Домен сильно связанный; скорость разработки и изменения системы были важнее распределённой архитектуры.
- Ничейные лиды + слотовое распределение. Принадлежность клиента определяется правилами распределения, а не тем, кто первый успел, — это снижало «перехваты» и перекосы.
- Разделение HTTP и WebSocket. Синхронный трафик — через gunicorn (WSGI), real-time — через daphne (ASGI) отдельным сервисом.
Права доступа
Была строгая ролевая модель:
- Менеджер — видит только своих клиентов и сделки, а также клиентов своего салона; не видит телефоны и детали чужих клиентов и не может с ними взаимодействовать.
- Управляющий салоном — видит всё внутри салона, управляет всеми клиентами и сделками своего подразделения.
- Управляющий отделением — видит всё внутри региона, управляет всеми салонами.
- Администратор — полный доступ ко всей системе, может действовать от имени пользователя через impersonation.
Поскольку наша CRM — это закрытая система только для нашей компании, админ мог по нажатию кнопки залогиниться любым пользователем. Это очень удобно для отладки: админ мог работать «глазами пользователя», при этом все действия логировались как «админ под пользователем», что давало полный аудит.
Инфраструктура и деплой
Всё разворачивалось через Docker Compose — пять сервисов, каждый со своей ролью:
- db — база MySQL, единый источник данных.
- redis — брокер очередей Celery, слой для WebSocket и хранилище служебных ключей для маршрутизации звонков.
- web — само Django-приложение, обрабатывает обычные HTTP-запросы.
- celery — фоновый воркер: периодические задачи, таймауты офферов, распределение лидов.
- ws — отдельный сервис под WebSocket, отвечает за real-time (например, телефонию).
Тестирование
Тесты писал в первую очередь на самую рискованную часть — конкурентное распределение лидов, где цена ошибки максимальна. Покрывал:
- работу со слотами: поиск, назначение лида менеджеру, откат при неудаче;
- механизм офферов: таймауты и приём лида;
- изоляцию чужих данных — менеджер не должен видеть чужих клиентов;
- конкурентную выдачу номеров под реальными транзакциями (гонки на счётчике);
- ключевые бизнес-инварианты — например, продажа не может быть оформлена без суммы.
Тесты запускались и без поднятой инфраструктуры (in-memory база, синхронный Celery), чтобы быстро прогонять именно логику, а не окружение.
Эксплуатация и мониторинг
Система работала в закрытой инфраструктуре, доступ был только через прокси, поэтому часть проблем решалась совместно с системным администратором.
Мониторинг и логирование включали:
- подробное логирование всех действий в CRM;
- аудит клиентов, лидов, взаимодействий;
- хранение логов около 7 дней;
- стек мониторинга Prometheus / Grafana / Loki.
На практике основным инструментом диагностики часто были именно бизнес-логи — они показывали контекст действий.
Фактически, я всегда был на связи при инцидентах, подключался к проблемам, разбирал причины и выкатывал фиксы; системный администратор помогал с инфраструктурой (сеть, прокси, сервер).
Выводы
- Самая дорогая часть переезда с legacy — не переписывание кода, а восстановление смысла в данных. Без уникальных идентификаторов на стороне источника миграция нормализуется только «по поведению системы».
- Для сильно связанного домена модульный монолит оказался правильным выбором: скорость изменений важнее распределённости.
- Конкурентную бизнес-логику (распределение лидов, слоты, обработку событий телефонии) нельзя писать без транзакционных гарантий — гонки здесь стоят двойных назначений и потерянных обращений.