Перейти к содержимому
VC
Кейс 32 из 33 · Интернет-торговля · внутренние системы

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

Общий почтовый ящик, живые данные Shopify, отгрузки, звонки, ответы опроса и возвраты по клиенту — в одном окне. Написан на стандартной библиотеке Python и работает на арендованном сервере с 1967 МБ памяти.

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

Ответ на вопрос «где мой заказ» собирался из пяти систем

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

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

Чарджбеки одного из магазинов не были видны вообще: они приходили в интерфейс, в который операционная команда просто не заходила. Узнавали о них постфактум.

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

02 · Решение

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

HTTP-сервер на http.server из стандартной библиотеки, без веб-фреймворка. Причина сугубо практическая: так он ставится на сервер, где нет pip и нет права его туда принести. Ни одной внешней зависимости — значит, нечему сломаться при обновлении и нечего чинить через полгода.

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

Проверка ящиков каждые 15 минут: два корпоративных адреса, 94 800 разобранных писем

02
Разбор

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

03
Индексы

Адрес → смещения строк; отдельный индекс номеров заказов по телу писем

04
Карточка

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

05
Ответ

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

Шесть источников параллельно — и честная деградация

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

Ключевая деталь — счётчик сбоев внутри каждого потока, который передаётся в главный. Без него отвалившийся источник просто не показывался бы, и неполная карточка молча считалась бы полной. Операционист видел бы «заказов нет» там, где на самом деле «магазин сейчас не ответил». Это разные вещи, и клиенту про них говорят разное.

Уход с чужой машины: локальные зеркала вместо похода по ssh

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

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

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

Безопасность внутреннего инструмента

Канал для меточных источников собирался из адреса, по которому пришёл покупатель, и попадал в разметку без фильтра. Значит, код постороннего человека исполнялся бы в браузере администратора — у которого в этот момент открыты карточки клиентов и отправка писем. Экранирование поставлено на границе разметки во всех шести местах.

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

Индексы вместо стрима всего корпуса

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

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

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

Потоковый разбор почты: иначе не хватает памяти

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

Корпус для базы знаний забирается с сервера готовым

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

Главное здесь — предохранитель против потери истории: файл заменяется, только если он не короче локального. Он сработал на первом же боевом прогоне — замену по второму ящику отклонил (8 022 против 7 985) и сохранил 34 письма, которых на сервере не хватало. Это тот же тезис, на котором держится весь кейс: неполный источник обязан объявлять себя неполным.

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

Всё, что нужно, чтобы не выходить из окна

  • Отправка и пересылка писем с вложениями через очередь — интерфейс не блокируется на SMTP
  • Персональные шаблоны и подписи у каждого сотрудника
  • AI-черновик ответа — по контексту треда и данным карточки; редактируется перед отправкой
  • Персональные аккаунты с памятью устройства — не логиниться заново каждое утро
  • Аудит-лог «кто / что / чем кончилось» — по любому действию видно, кто его сделал и чем оно завершилось
  • Блок складских остатков из Lark — прямо в карточке, без отдельной таблицы
  • Полноценная мобильная вёрстка — проверено на 390px, отвечать можно с телефона
  • Экран «Гипотезы» — план против факта с историей снимков; каждый снимок ложится в журнал, а линия тренда появляется со второй точки
  • Экран «Инфра и подписки» — что оплачено, до какого числа и во сколько обходится месяц
  • Экран «Что говорят покупатели» — ответы послепродажного опроса; отдельный тест краснеет, если в разметку попадёт текст ответа покупателя или адрес почты

Шесть аналитических экранов на движке вечерней сводки

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

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

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

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

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

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

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

Эксплуатация: сторожа переехали на сервер

Два сторожа — зависших заказов и спорных списаний — переведены с ноутбука под systemd-таймеры на сервере. На ноутбуке сторож заказов был мёртв: у процессов под launchd нет доступа к ключу, процесс выходил с ошибкой и писал «источник не отдал заказов — молчу», и это молчание читалось как «зависших заказов нет».

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

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

03 · Стек

Ноль внешних зависимостей — осознанно

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

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

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 записей — потолок каждого кэша, самые старые вытесняются

Замер: 1,16 МБ на окно в 30 дней и 2,7 МБ на 90 дней; достижимых окон 2415 при практическом пороге около 410 — перебрать их человек может за один вечер, просто щёлкая даты. На сервере включена блокировка перебора по ssh: за сутки 1633 обрыва и 136 сбросов соединений, 736 попыток с одного чужого адреса; после этого выгрузки перестали получать разрывы.

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

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

Переезд на выделенный сервер занял один день и был принят по паритету со старой инсталляцией. Сегодня контур CRM, аналитики и отчётности держит 2471 зелёный тест — на начало периода их было около 500; ночной прогон на сервере даёт 749 пройденных при нуле упавших. Для внутреннего инструмента это уровень продукта, за который отвечают.

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

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

Где ещё ложится та же методология

За кейсом стоит типовая задача «склеить N систем в один экран вокруг одной сущности» — там, где сотрудник каждый день руками сводит данные из разных вкладок:

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

Если сотрудник делает join между вкладками руками — это собирается в один экран

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

Готовы начать?

Аудит за 9 900 ₽ — с конкретным отчётом и сметой

Расскажу что внедрить в вашем бизнесе в первую очередь, какая будет окупаемость, и нужен ли вообще AI для вашей задачи (иногда — нет).

Или просто напишите свой вопрос — отвечу в течение 2 часов