Внутренняя CRM для операционной команды: один экран вместо пяти вкладок
Общий почтовый ящик, живые данные Shopify, отгрузки, звонки, ответы опроса и возвраты по клиенту — в одном окне. Написан на стандартной библиотеке Python и работает на дроплете с 961 МБ памяти.
Ответ на вопрос «где мой заказ» собирался из пяти систем
US-бренд товаров для активного отдыха, два магазина на Shopify. Двое операционистов вели клиентов, переключаясь между двумя админками Shopify, почтовым клиентом, облачной телефонией и складскими таблицами.
Самый частый вопрос покупателя — «где мой заказ». Чтобы на него ответить, нужно было найти письмо, вытащить из него номер заказа, вспомнить, в каком из двух магазинов этот заказ лежит, открыть нужную админку, найти отгрузку, вернуться в почту и написать ответ. Пять переключений контекста на одну строчку текста.
Чарджбеки одного из магазинов не были видны вообще: они приходили в интерфейс, в который операционная команда просто не заходила. Узнавали о них постфактум.
Первая версия карточки клиента, когда её собрали, грузилась 10 секунд — и операционист сказал об этом прямо. Это и стало главным техническим требованием: инструмент, который открывается дольше, чем ручное переключение вкладок, никто использовать не будет.
Своя CRM на стандартной библиотеке Python
HTTP-сервер на
http.server
из стандартной библиотеки, без веб-фреймворка. Не из эстетики: так он ставится на сервер,
где нет pip и нет права его туда принести. Ни одной внешней зависимости — значит, нечему
сломаться при обновлении и нечего чинить через полгода.
IMAP каждые 15 минут: два корпоративных ящика, более 100 тыс. писем
Разбор в треды, классификация, потоковая запись на диск
Адрес → смещения строк; отдельный индекс номеров заказов по телу писем
Шесть источников параллельно: заказы обоих магазинов, отгрузки, звонки, опрос, возвраты
Шаблоны, подписи, AI-черновик, отправка и пересылка с вложениями через очередь
Шесть источников параллельно — и честная деградация
Карточка клиента собирает шесть независимых источников одновременно: заказы первого магазина, заказы второго, отгрузки, историю звонков из облачной телефонии, ответы на пост-продажный опрос и возвраты с чарджбеками.
Ключевая деталь — thread-local счётчик сбоев, который переносится в главный поток. Без него отвалившийся источник просто не показывался бы, и неполная карточка молча считалась бы полной. Операционист видел бы «заказов нет» там, где на самом деле «API магазина сейчас не ответил». Это разные вещи, и клиенту про них говорят разное.
Индексы вместо стрима всего корпуса
Почта двух ящиков — более 100 тыс. писем, корпус в сотни мегабайт. Первая версия на каждый просмотр карточки читала его целиком: отсюда 5,8 секунды на поиск писем по клиенту.
Построил два индекса:
- Адрес → смещения строк — открыть корпус, прыгнуть по offset, прочитать только нужные письма
- Номер заказа → клиент — индекс номеров, вытащенных из тел писем: покупатель пишет с личного адреса, а заказ оформлен на рабочий — карточка всё равно находится
Потоковый парсер: иначе OOM-kill
Парсер почты изначально держал разобранный корпус в памяти — на дроплете с 961 МБ это заканчивалось OOM-kill. Переписал на потоковый: письма проливаются на диск по одному, в памяти остаётся только лёгкая проекция заголовков. Пик памяти упал с 985 МБ до 256 МБ (на самом сервере — 215 МБ), при этом результат побайтово идентичен: то же количество писем и тредов, sha256 содержимого совпал.
Всё, что нужно, чтобы не выходить из окна
- Отправка и пересылка писем с вложениями через очередь — интерфейс не блокируется на SMTP
- Персональные шаблоны и подписи у каждого сотрудника
- AI-черновик ответа — по контексту треда и данным карточки; редактируется перед отправкой
- Персональные аккаунты с памятью устройства — не логиниться заново каждое утро
- Аудит-лог «кто / что / чем кончилось» — по любому действию видно, кто его сделал и чем оно завершилось
- Блок складских остатков из Lark — прямо в карточке, без отдельной таблицы
- Полноценная мобильная вёрстка — проверено на 390px, отвечать можно с телефона
Переезд на выделенный сервер — за один день
Сначала CRM жила на общем сервере вместе с трекером — соседство, при котором любой всплеск нагрузки бьёт по обоим. Вынес на отдельный сервер за nginx + Let’s Encrypt, с 15-минутным IMAP-синком, ежечасным монитором и ночными бэкапами. Переезд сверен паритетом: письма, треды и заказы на дашборде сошлись один в один со старой инсталляцией по каждому счётчику.
Ноль внешних зависимостей — осознанно
http.server, imaplib, sqlite3, urllib — ставится на сервер, где нет pip
Живые заказы и отгрузки двух магазинов, без промежуточной базы
Собственный парсер, классификатор и индексатор почты
Черновик ответа по контексту треда — сотрудник правит и отправляет
Выделенный сервер, TLS через certbot, синк и мониторы по таймерам
454 собираемых теста, 28 тестовых файлов при 15 модулях кода
Замеры до и после
шесть источников, собранные параллельно
индекс смещений вместо чтения всего корпуса
на самом сервере 215 МБ; результат побайтово идентичен
Операционная команда работает в одном окне: письмо, заказы обоих магазинов, отгрузка, звонки, ответы опроса и возвраты — на одном экране, с уже подготовленным текстом ответа. Чарджбеки перестали быть слепой зоной — они лежат в той же карточке, что и всё остальное по клиенту.
Оптимизация памяти проверялась не «на глаз»: после переписывания парсера сверялись количество писем, количество тредов и sha256 содержимого — совпали. Это тот случай, когда ускорение не имеет права поменять данные, и это надо доказать, а не заявить.
Переезд на выделенный сервер занял один день и был принят по паритету со старой инсталляцией. На проекте — 120 коммитов в модуле CRM и 454 собираемых теста (28 тестовых файлов при 15 модулях кода): внутренний инструмент, но не «скрипт на коленке».
Где ещё ложится та же методология
Это не «CRM для интернет-магазина». Это типовая задача «склеить N систем в один экран вокруг одной сущности» — там, где сотрудник каждый день руками делает join между вкладками:
- → Поддержка на общем ящике — почта + учётная система + доставка в одном треде вместо трёх вкладок
- → Мультиканальные продажи — несколько витрин и маркетплейсов, у каждого своя админка, а клиент один
- → Сервис и выездные работы — заявка, история звонков, склад запчастей и статус наряда по одному клиенту
- → Архивы переписки как источник данных — индексация корпуса писем и поиск по номерам документов внутри тел
- → Замена коробочного helpdesk — когда абонплата за seat растёт, а нужны две-три специфичные вкладки, которых в коробке нет
- Параллельный сбор карточки с честным учётом сбоев — неполные данные никогда не выдаются за полные
- Индексатор почтового корпуса: адрес → смещения и номер документа → клиент
- Потоковая обработка вместо «загрузить всё в память» — работает на самых дешёвых VPS
- Проверка миграции паритетом счётчиков и хешей, а не «вроде открылось»
- Аудит-лог действий сотрудников по схеме «кто / что / чем кончилось»
Если сотрудник делает join между вкладками руками — это собирается в один экран
Начинаем с самого частого вопроса, на который команда отвечает каждый день, и собираем ровно тот экран, который на него отвечает. Первая рабочая версия — недели, а не кварталы, и её сразу проверяет тот, кто в ней будет сидеть.
Аудит за 5 000 ₽ — с конкретным отчётом и сметой
Расскажу что внедрить в вашем бизнесе в первую очередь, какая будет окупаемость, и нужен ли вообще AI для вашей задачи (иногда — нет).
Или просто напишите свой вопрос — отвечу в течение 2 часов