CRM Rewrite

Замена критичной для бизнеса 1С-CRM люксового ювелирного бренда на модульный монолит на Django. Самым сложным была не разработка, а нормализация и миграция грязных данных из старой системы.

Подключил телефонию через АТС MangoНастроил интеграцию с Яндекс.Метрикой для анализа спамаУспешно мигрировал данные из легаси

Контекст

Разрабатывал корпоративную 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 ещё какое-то время оставалась как резервная система, но основная работа уже шла в новой.

Ключевые архитектурные решения

  1. Транзакционность. Активно использовал atomic, блокировки и on_commit, потому что в системе было много гонок, особенно вокруг распределения лидов и обработки событий телефонии.
  2. Celery как ядро асинхронности. Центральный механизм всей асинхронной логики — от лидов до интеграций.
  3. Сознательный отказ от микросервисов. Домен сильно связанный; скорость разработки и изменения системы были важнее распределённой архитектуры.
  4. Ничейные лиды + слотовое распределение. Принадлежность клиента определяется правилами распределения, а не тем, кто первый успел, — это снижало «перехваты» и перекосы.
  5. Разделение 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 — не переписывание кода, а восстановление смысла в данных. Без уникальных идентификаторов на стороне источника миграция нормализуется только «по поведению системы».
  • Для сильно связанного домена модульный монолит оказался правильным выбором: скорость изменений важнее распределённости.
  • Конкурентную бизнес-логику (распределение лидов, слоты, обработку событий телефонии) нельзя писать без транзакционных гарантий — гонки здесь стоят двойных назначений и потерянных обращений.
← все кейсы