Перейти к содержимому

Разрешить сайту считать ваши посещения с помощью Яндекс Метрики? Для этого она сохранит в браузере файлы cookie. Как сайт использует cookie

VC
Кейс 24 из 25 · Интернет-торговля · внутренние системы

Своя CRM для операционной команды: пять систем собраны на одном экране

Всё о клиенте теперь в одном окне: общий почтовый ящик, свежие данные из Shopify, отгрузки, звонки, ответы на опрос и возвраты. CRM написана на стандартной библиотеке Python, без сторонних пакетов, и работает на небольшом арендованном сервере с 1967 МБ памяти.

Отрасль
Интернет-торговля, два магазина на Shopify
Технологии
Python · Shopify Admin API · IMAP
Сроки
≈ 6 недель до запуска в работу
Итог
Карточка клиента: 10,3 с → ≈3 с
01 · Проблема

Чтобы ответить «где мой заказ», приходилось открывать пять систем

Бренд товаров для активного отдыха из США, два магазина на Shopify. Клиентов вели двое сотрудников операционной команды. Им всё время приходилось переключаться между двумя админками Shopify, почтой, облачной телефонией и складскими таблицами.

Чаще всего покупатель спрашивает: «где мой заказ?» Чтобы ответить, нужно было найти письмо, достать из него номер заказа, вспомнить, в каком из двух магазинов этот заказ, открыть нужную админку, найти отгрузку, вернуться в почту и написать ответ. Пять переходов между окнами ради одной строчки.

Чарджбеки одного из магазинов, то есть платежи, которые покупатель оспорил через банк, не видел никто. Они приходили в раздел, куда команда не заходила, и о них узнавали задним числом.

Первая версия карточки клиента открывалась 10 секунд, и сотрудник сразу об этом сказал. Отсюда главное техническое требование: если инструмент открывается дольше, чем человек переключает вкладки вручную, им не будут пользоваться.

02 · Решение

Своя CRM на стандартной библиотеке Python

Веб-сервер собран на http.server из стандартной библиотеки, без фреймворка. Причина практическая: так CRM ставится на сервер, где нет pip (установщика пакетов Python) и нет права его поставить. Сторонних библиотек нет, поэтому обновления сервера почти ничего не могут в ней сломать.

01
Проверка почты

Каждые 15 минут CRM проверяет два рабочих ящика; в большем из них 94 800 разобранных писем

02
Разбор

Письма собираются в переписки, сортируются и записываются на диск по мере чтения

03
Индексы

Для каждого адреса известно, где в архиве лежат его письма; отдельно собраны номера заказов из текста писем

04
Карточка

Опрашивает шесть источников сразу: заказы обоих магазинов, отгрузки, звонки, опрос, возвраты

05
Ответ

Шаблоны, подписи, черновик от ИИ, отправка и пересылка с вложениями через очередь

Карточка опрашивает шесть источников сразу и показывает, какой не ответил

Карточка клиента обращается к шести независимым источникам одновременно: к заказам первого магазина, заказам второго, отгрузкам, истории звонков в облачной телефонии, ответам на опрос после покупки и возвратам вместе со спорами по платежам.

Главная деталь в том, что каждый источник сообщает о своём сбое. Иначе отвалившийся источник просто исчез бы с экрана, и неполная карточка выглядела бы полной. Сотрудник видел бы «заказов нет», хотя на самом деле магазин в этот момент не ответил. Клиенту в этих двух случаях говорят разное.

Данные со вспомогательного сервера теперь лежат рядом с CRM

Часть данных CRM брала со вспомогательного сервера и при каждом открытии карточки подключалась к нему по ssh. Короткий обрыв связи в этот момент давал ту самую ошибку: на экране «заказов нет», хотя источник просто не ответил.

Теперь копия базы, где записано, откуда пришёл каждый покупатель, хранится на сервере CRM и обновляется раз в 10 минут. Снимок базы создаёт штатное средство резервного копирования на стороне источника. Если просто скопировать файл, в который в этот момент идёт запись, получится битая копия: она откроется, но покажет неверные данные. Поэтому копия принимается, только если база читается и её размер правдоподобен, а заменяется она одним шагом.

Заказы обоих магазинов CRM теперь берёт прямо из Shopify. Ключи доступа лежат на её сервере по тем же путям, что и на вспомогательном. Поэтому один и тот же скрипт работает и на месте, и по ssh, и восемь функций карточки переписывать не пришлось. Прямой путь включается, только когда есть ключи обоих магазинов: с ключом от одного картина выглядела бы правдоподобной, но была бы урезанной. Если копия устарела, CRM возвращается к прежнему, медленному пути и записывает это в журнал.

На первом рабочем прогоне копия весила 1,8 МБ и содержала 777 покупок. У всех 42 покупок за трое суток источник был известен. Расчёт по локальным копиям совпал с прежним до байта. На каждую часть написано по 5 тестов, включая защиту от подстановки чужого кода в запрос к базе. До правки все они падали.

Внутренний инструмент тоже нужно защищать

Название источника, откуда пришёл покупатель, бралось из ссылки и вставлялось в страницу без обработки. Значит, посторонний мог подложить в ссылку код, и тот выполнился бы в браузере администратора, у которого открыты карточки клиентов и отправка писем. Теперь спецсимволы обезвреживаются во всех шести местах, где это название попадает на страницу.

Вторая правка касалась сессий. После смены пароля продолжали работать 34 открытые сессии из 38, из них 26 администраторских. Отпечаток сессии был длиной 16 символов, и на соль (случайную добавку) приходилось всего два из них: 256 возможных значений на 4000 хешей. Теперь длина отпечатка 48 символов. Обе правки проверены мутационным тестированием: на каждую написано по два теста, которые падают, если правку убрать.

Индексы: CRM читает только нужные письма

В двух ящиках больше 100 тысяч писем, архив занимает сотни мегабайт. Первая версия при каждом открытии карточки читала его целиком, отсюда 5,8 секунды на поиск писем клиента.

Я построил два индекса:

  • Адрес → место в архиве. CRM открывает архив, сразу переходит к нужному месту и читает только письма этого клиента
  • Номер заказа → клиент. Номера заказов собраны из текста писем. Если покупатель пишет с личного адреса, а заказ оформил на рабочий, карточка всё равно находится

Почта разбирается по одному письму, иначе не хватает памяти

Сначала разбор держал все письма в памяти. На прежнем сервере с 961 МБ система из-за этого принудительно завершала процесс. Я переписал разбор: теперь письма записываются на диск по одному, а в памяти остаётся только краткая выжимка заголовков. Пик памяти упал с 985 до 256 МБ (на самом сервере — 215 МБ). Результат совпал до байта: столько же писем и переписок, та же контрольная сумма содержимого (sha256).

База знаний получает почту с сервера уже разобранной

Рабочий компьютер скачивал 34 ГБ сырой почты ради 459 МБ разобранного архива. Между тем на сервере такой архив уже есть: он весит 450 МБ и в целом полнее локального. Скачивание переключается отдельно для каждого ящика, поэтому переводить оба сразу не нужно.

Главное здесь — защита от потери истории: файл заменяется, только если новый не короче локального. Она сработала на первом же рабочем прогоне и не дала заменить архив второго ящика: локально в нём было 8 022 письма, на сервере 7 985. Письма, которых на сервере не хватало, сохранились. На этом правиле держится весь кейс: неполный источник должен сообщать, что он неполный.

Заодно по замечанию сотрудника поддержки пересмотрен показатель «упущенные заявки». Оказалось, что 82% выборки — автоматические уведомления магазина и склада. А среди писем от людей почти половина оставшихся без ответа — благодарности. Ещё отключена сборка индекса на 103 МБ: она шла каждые 15 минут, хотя индексом никто не пользовался.

Всё нужное — в одном окне

  • Отправка и пересылка писем с вложениями через очередь: интерфейс не зависает, пока письмо уходит
  • Свои шаблоны и подписи у каждого сотрудника
  • Черновик ответа от ИИ по всей переписке и данным карточки; перед отправкой его можно поправить
  • Личные учётные записи, которые запоминают устройство: не нужно входить заново каждое утро
  • Журнал действий: по любому действию видно, кто его сделал и чем оно закончилось
  • Складские остатки из таблиц Lark прямо в карточке
  • Удобная мобильная версия, проверенная на экране шириной 390 пикселей: отвечать можно с телефона
  • Экран «Гипотезы»: план и факт с историей снимков. Каждый снимок записывается в журнал, линия тренда появляется со второй точки
  • Экран «Инфра и подписки»: что оплачено, до какого числа и сколько стоит месяц
  • Экран «Что говорят покупатели»: ответы на опрос после покупки. Отдельный тест падает, если на страницу попадёт текст ответа покупателя или адрес его почты

Шесть экранов аналитики считают так же, как вечерняя сводка

Внутри CRM появилась своя аналитика: экраны «День», «Период», «Товары», «Ссылки», «Пробелы» и «Гипотезы». Можно выбрать любые даты или готовый календарный период, сравнить его с предыдущим и выгрузить в CSV, который Excel открывает сразу. Доступ к экранам есть только у администратора.

Главное решение здесь такое: экраны считают тем же кодом, что и вечерняя сводка. Два независимых подсчёта дали бы две разные выручки, и спор о том, какая из них верная, отнял бы больше времени, чем сами экраны.

Замеры: первый заход занимал 6,9 с, теперь экран открывается мгновенно, потому что данные загружаются один раз, при перезапуске сервиса. Вкладка «Товары» собирается с нуля за 7,5 с, а из кэша — меньше чем за 0,01 с. Сумма по товарам сходится с общей выручкой до цента, а доли в каждом фильтре складываются ровно в 100%.

  • Один подсчёт для товаров. Модуль, который строил список самых продаваемых товаров, считал по-своему и завышал выручку на 8,54%. Я перевёл его на общий код подсчёта, и сверка за 30 дней на рабочих данных (324 заказа) показала расхождение 0,00. Строки сводятся по идентификатору товара: артикул заполнен лишь у 12% строк, идентификатор — у 97%
  • Список гипотез «план и факт». В нём 7 гипотез с историей снимков. В коде закреплены три правила. Первое: вердикт выносится только после окончания срока, потому что покупку здесь обдумывают 2–3 месяца. Второе: вывод делается, только когда наблюдений достаточно. Третье: продажа, пришедшая из источника, записывается на него, но дополнительной эту выручку можно назвать только с отдельным доказательством. Ещё одна проверка требует указать в примечании, кто утвердил цель
  • Состояние интеграций. Страница показывала только 5 сервисов из 14. Я подключил остальные 9, и у каждого сервиса теперь 5 возможных состояний. Если сервис не ответил, на экране остаётся его последний успешный результат
  • Сравнение периодов. Сравнение ошибалось, когда данные покрывали период лишь частично. Предыдущий период, где было всего 8 дней данных, давал рост выручки на 320% на ровном месте. А период «90 дней» делил выручку за 69 дней на 90 и занижал среднюю выручку за день на 23%

Обе уязвимости, о которых шла речь выше, — код в названии источника и сессии, пережившие смену пароля, — я нашёл, когда работал над этими экранами и разбирал собственный код.

Переезд на отдельный сервер занял один день

Сначала CRM работала на общем сервере вместе с трекером рекламы, и любой всплеск нагрузки сказывался на обоих. Я перенёс её на отдельный сервер и настроил шифрованное соединение (nginx и сертификаты Let’s Encrypt). Почта проверяется каждые 15 минут, работоспособность — раз в час, резервные копии делаются по ночам. Переезд проверили по счётчикам: письма, переписки и заказы на экране совпали со старой установкой один в один.

Сторожевые скрипты переехали на сервер

Два сторожевых скрипта следят за зависшими заказами и спорными списаниями. Их перенесли с ноутбука на сервер и запускают по расписанию (таймеры systemd). На ноутбуке сторож заказов на самом деле не работал: фоновым задачам macOS (launchd) не был доступен ключ. Скрипт завершался с ошибкой и писал «источник не отдал заказов — молчу», а это молчание принимали за «зависших заказов нет».

Первый же прогон на сервере нашёл три заказа, которые действительно зависли: они висели 11, 11 и 5 дней. Сторож споров при первом запуске увидел 32 спора и, как и задумано, просто запомнил их, чтобы не поднимать тревогу по старым спорам. Теперь и за сроком ответа на спор по чарджбеку следит расписание.

Там же в CRM появилась страница расходов на серверы и подписки. У каждой услуги можно указать дату отключения. Отключённая услуга пропадает из месячного расхода и из напоминаний, остаётся в накопленных тратах, но больше их не увеличивает. Если блок пустой, на экране написано почему. Есть и защита от ложного нуля: если серверы есть, а сумма нулевая, на экране стоит прочерк с объяснением, потому что по этой странице сверяют счета. Восстановить эти данные задним числом нельзя, и до появления страницы на вопрос «сколько мы тратили в прошлом месяце» ответа не было.

03 · Технологии

Python без сторонних библиотек — так задумано

Стандартная библиотека Python

http.server, imaplib, sqlite3, urllib: CRM ставится даже на сервер, где нельзя доустанавливать пакеты

Shopify Admin API

Свежие заказы и отгрузки двух магазинов, без промежуточной базы

IMAP / SMTP

Свой разбор, сортировка и индексация почты

Языковая модель

Черновик ответа по всей переписке: сотрудник правит его и отправляет

systemd · nginx · Let's Encrypt

Отдельный сервер, шифрование через certbot, проверки и мониторинг по расписанию

pytest

2471 тест по CRM, аналитике и отчётности, все проходят; ночной прогон на сервере — 749 тестов, ни одного упавшего

Python stdlibhttp.serverimaplibsqlite3Shopify Admin APIIMAP/SMTPLLMsystemdnginxLet's Encryptpytest
04 · Результат

Замеры до и после

Карточка клиента
10,3 с ≈3 с

шесть источников опрашиваются одновременно

Поиск писем клиента
5,8 с 0,03–0,45 с

по индексу, без чтения всего архива

Пик памяти при разборе почты
985 МБ 256 МБ

на самом сервере 215 МБ; результат совпал до байта

Устойчивость под нагрузкой
120 записей — предел каждого кэша; самые старые удаляются первыми

Замер: период в 30 дней занимает в кэше 1,16 МБ, в 90 дней — 2,7 МБ. Всего можно выбрать 2415 разных периодов, а опасный порог — около 410. Столько человек переберёт за один вечер, просто щёлкая даты.

Ещё на сервере включена защита от подбора паролей по ssh. До неё за сутки набиралось 1633 обрыва и 136 сбросов соединения, а 736 попыток входа шли с одного чужого адреса. После блокировки выгрузки данных перестали обрываться.

Операционная команда работает в одном окне: письмо, заказы обоих магазинов, отгрузка, звонки, ответы на опрос, возвраты и уже готовый черновик ответа. Чарджбеки больше не теряются: они лежат в той же карточке, что и всё остальное о клиенте.

Экономию памяти проверяли подсчётом. После переписывания разбора сверили количество писем, количество переписок и контрольную сумму содержимого, и всё совпало. Такая оптимизация не должна менять данные, и это нужно было доказать.

Переезд на отдельный сервер занял один день; его приняли, когда счётчики сошлись со старой установкой. Сейчас CRM, аналитику и отчётность проверяет 2471 тест, и все проходят. В начале работы тестов было около 500. Ночной прогон на сервере: 749 тестов, ни одного упавшего.

В работу CRM вошла примерно через шесть недель, и я продолжаю её развивать. За последние три недели она стала рабочим местом всей операционной команды: появились экраны аналитики, список гипотез, страница расходов на серверы и подписки, сторожевые скрипты на сервере и локальные копии источников.

05 · Применимость

Где ещё работает тот же подход

За кейсом стоит типовая задача: «собрать данные из нескольких систем на одном экране вокруг одного клиента, заказа или объекта». Она возникает везде, где сотрудник каждый день вручную сводит данные из разных вкладок:

  • → Поддержка на общем ящике — письмо, данные учётной системы и статус доставки на одном экране
  • → Продажи в нескольких каналах — несколько витрин и маркетплейсов, у каждого своя админка, а клиент один
  • → Сервис и выездные работы — заявка, история звонков, склад запчастей и статус наряда по одному клиенту
  • → Архив переписки как источник данных — индексация писем и поиск по номерам документов в их тексте
  • → Замена готовой системы поддержки — когда плата за каждого сотрудника растёт, а нужны две-три особые вкладки, которых в готовом решении нет
Что переиспользуется на следующих проектах
  • Сбор карточки из нескольких источников сразу с честным учётом сбоев: неполные данные не выдаются за полные
  • Индексация всего архива почты: адрес → место в файле, номер документа → клиент
  • Обработка по одному письму, без загрузки всего архива в память: хватает недорогого сервера
  • Проверка переезда сверкой счётчиков и контрольных сумм
  • Журнал действий сотрудников: кто что сделал и чем это кончилось

Обновление работы в октябре 2026 года

Осенью 2026 года добавил проверки свежести общей почты и подтверждения доставки. Пустой результат поиска, устаревший снимок и ошибка чтения показываются разными состояниями. Очередь писем без ответа учитывает фактическую переписку по тому же адресу.

Для фоновых операций появились ограниченные повторы и проверка результата. Работа с крупным почтовым архивом и резервными копиями вынесена в отдельные проверки; доступ к операциям определяется ролью и маршрутом запроса.

Похожая задача?

Если сотрудник сводит данные из разных вкладок вручную, их можно собрать на одном экране

Я начинаю с самого частого вопроса, на который команда отвечает каждый день, и собираю экран, который отвечает именно на него. Первая рабочая версия появляется через несколько недель, и её сразу проверяет тот, кто будет в ней работать.