Как мы CRM Битрикс24 с кучей всего интегрировали
У нас был сложный сайт с личным кабинетом клиентов, устаревшая, переписанная 1С-ка, десяток маркетинговых сервисов, и телефония на Asterisk.
Единственное, что вызывало у меня опасение — это учётная система, написанная на .net. Ничто в мире не бывает более беспомощным, безответственным и порочным, чем php-программист, который пытается написать интеграцию с .net. Я знал, что рано или поздно мы заинтегрируем и эту дрянь…
Меня зовут Антон, я руковожу проектами по внедрениям CRM Битрикс24 в компании ИНТЕРВОЛГА.
Сегодня расскажем, как мы не сошли с ума, пытаясь подружить новую CRM Битрикс24 с зоопарком клиента.
Если вы ИТ-специалист, которого попросили организовать внедрение CRM, то наверняка в пожеланиях была фраза про «пусть менеджеры работают в одной программе, только в CRM». Никаких переключений в 1С и другие системы. Чтобы реализовать такой сценарий, нужно обогатить CRM данными из внешних систем, а потом добавить логику работы с этими данными.
В CRM будет появляться новая информация, и ее придется передавать в другие системы.
Итак, в качестве нового зверька в зоопарке сегодня выступает 1С-Битрикс24.
Тут многие могут спросить, а чего его интегрировать, там же »1С» в названии есть, уж хотя бы с 1Ской всё должно работать «из коробки».
интеграция 1C
Действительно, для стандартных ситуаций есть готовые механизмы обмена. Собрали для вас небольшую табличку таких заготовок «из коробки».
В нашем кейсе для NDA-компании (импортёра) мы должны были организовать CRM для оптовых b2b-продаж. Структура продаж выглядела так:
Система работы b2b-продаж
Структура стандартная, если вы что-то импортируете или производите в больших количествах. Например, вы официальный импортёр (или производитель) Нюка-Колы. Ваша продукция пользуется спросом по всей стране, крышечки особенно.
Продавать Нюка-Колу напрямую каждому магазинчику и бару слишком хлопотно. Поэтому, вы сотрудничаете с крупными дистрибьюторами. Те, в свою очередь, продают напиток мелкому бизнесу. А уже у него вы, как розничный клиент, покупаете Нюка-Колу по дороге на работу, чтобы взбодриться.
Путь продукции в целом понятен, почти на каждом этапе (возможно, за исключением розничной продажи), нужно принимать и отгружать заказы, контролировать план продаж, работать с претензиями клиентов и направлять закрывающие документы.
За каждый из таких блоков отвечает та или иная ИТ-система. Нашей задачей было добавить сюда CRM-систему, отслеживающую продажи дистрибьюторов другим b2b клиентам. В этой статье мы хотим остановиться на интеграционной составляющей такого проекта — обычно это наиболее рискованная и сложная часть.
Чтобы реализовать проект, нужно интегрировать нашего новичка со старожилами:
Развитый сайт с личным кабинетом b2b-клиента.
Система по обмену данными с дистрибьюторами Pradata.
Несколько разноплановых переписанных 1С-ок крупных дистрибьюторов.
Самописная учётная система одного из дистрибьюторов.
Несколько маркетинговых внешних сервисов, оставим их пока за рамками.
Передавать нужно было стандартный для таких проектов состав сущностей:
Товары.
Клиенты.
Контакты.
Заказы.
Дополнительные справочники.
Как обычно, усложнялось всё сроками и большим количеством участников проекта. Готового модуля для такого случая у Битрикса ожидаемо не нашлось. С учётом мнения всех сторон, выбрали следующую схему интеграций:
схема интеграций
Разберём, как мы «строили стрелочки».
Интеграция между 1Сками дистрибьюторов и CRM — промежуточная 1С
Схема с добавлением новой промежуточной 1С была выбрана для стабилизации разношёрстных данных от разных дистрибьюторов. Наша команда работала на стороне CRM и организовала промежуточную 1С с механизмами обмена.
Сначала планировали использовать встроенные в 1С планы обмена. Но потом для унификации решили всё сделать на веб-сервисах. Это позволило унифицировать обмен — 1Ски и CRM пользовались одними и теми же методами для общения с центральной 1Ской.
Обмен между 1С и CRM
Со стороны промежуточной 1С мы предоставили готовый REST-интерфейс с определенным набором методов: добавление товаров, а также добавление, изменение и удаление клиентов, менеджеров, типов цен, цен и заказов. Работа велась в конфигурации УТ 11.4.
Мы не хотели изобретать велосипед и планировали использовать стандартный протокол OData. Достаточно было опубликовать базу на веб-сервере и привязать RLS (ограничение доступа на уровне записей и полей).
После анализа поняли, что это не лучшее решение.
Интерфейс должен быть понятен и прост в использовании. Если взглянуть на изменение цены товара, то для этого необходимо создать документ «Установка цен номенклатуры» с заполнением всех обязательных полей (статус документа, статус согласования, ответственный и пр.).
Конечному пользователю не нужно знать об этих нюансах, ведь для автоматического создания документа ему достаточно передавать только дату изменения цены, ид товара и саму цену. Обработку полученной информации мы берем на себя.
Также был вопрос с логами — хотели сделать их удобными для просмотра пользователем. Обычно для этого используют журнал регистрации — специальный инструмент в 1С, который позволяет отслеживать кто и когда вносил изменения в базу. Он хорош собой, но не каждый менеджер сможет с ним эффективно взаимодействовать. Плюс хочется сохранять текст запроса для последующего анализа. Решили создать собственную систему логирования в регистре сведений. Это стандартный объект 1С для работы с разнообразными данными.
Учитывая все детали выше, для решения выбрали собственный REST-интерфейс.
Программный REST-интерфейс в 1С
По просьбе клиента были разграничены методы добавления и обновления, а для каждого метода были использованы свои шаблоны URL:
добавление клиента в 1С
В модуле http-сервиса только получение данных из http запроса и передача в метод обработки:
метод обработки
Прием и обработка полученных запросов была выделена в два общих модуля. Один из модулей выступает в роли REST-интерфейса контроллера. В этом модуле каждой из функций на вход подается строка в формате JSON или параметры, все зависит от метода. Каждая функция возвращает исключительно http ответ.
возвращение http-ответов
Вышеописанные функции схожи между собой, и каждая из них состоит из трех частей:
описание функции
«Внутренняя» обработка полученных данных происходит во втором модуле. Модуль разделен по областям на основании обрабатываемых объектов. Структуры областей схожи между собой:
структуры областей
Пример метода обновления типов (видов) цен. Сами методы также структурно схожи между собой:
метод обновления типов (видов) цен
Как мы видим, для использования методов из модуля-контроллера, достаточно передавать строки или параметры. Благодаря этому в «обработке» 1С без проблем удалось написать простой эмулятор отправки запросов, который используется для отладки, а также для обработки данных в 1С если, по той или иной причине, не работают HTTP-сервисы (напрямую в 1С данные не изменяем).
Пример работы эмулятора:
работа эмулятора
Под капотом:
Настроили логирование входящих запросов с разграничением по пользователям. Запросы, пришедшие с помощью эмулятора, также логируются.
Пример лога (специально передал некорректный JSON):
лог
Не забыли и про регистрацию изменений. Было решено сохранять дату и время изменения и дать возможность получать измененные данные на определенный период. Такой подход позволял повторно отправлять данные из 1С, если по какой-либо причине они были некорректно обработаны на стороне пользователей.
сохранение даты и времени изменения
В итоге написали готовый для использования REST-интерфейс. Все работы выполнялись в расширении, основная конфигурация осталась без изменений.
Это значит, что при следующем обновлении 1С, ничего из стандартных функций не сломается и вы «не слетите» с поддержки.
Выделим ещё несколько интересных доработок:
Сохранение трех версий табличной части товаров у заказов при различных статусах с удобным интерфейсом для просмотра.
Автоматическую загрузку вариантов статуса заказа с сайта и использование их идентификаторов в обмене.
Тут также не было готовых инструментов, собирали эти сценарии на основе типового инструмента версионирования объектов в 1С, а загрузку статусов вырезали из модуля обмена с БУСом.
Интеграция между CRM и личным кабинетом b2b-клиента
Личный кабинет был на 1С-Битрикс. Казалось бы, у сайта на Битриксе и у CRM Битрикс24 должны быть готовые механизмы интеграции. Они есть и закрывают самые базовые сценарии:
Передачу заявок с несложных форм.
Передачу заказов с сайта в CRM.
Но никакого обмена клиентами, контактами, товарами и т.д. нет. Может оно и хорошо, иначе кто тогда будет платить программистам? :)
Провели аналитику и поняли, что нагрузка будет высокой, а требования к доставке — серьезными. Решили использовать RabbitMQ. Дополнительно брокер сообщений должен помочь при подключении новых систем — в перспективе было ещё несколько сайтов и мобильное приложение.
Для работы с RMQ использовали стороннюю библиотеку php-amqplib/php-amqplib, рядом положили opis/json-schema для валидации сообщений и ramsey/uuid для генерации уникальных идентификаторов.
conn = AMQPSSLConnection::create_connection(
array_map(
fn(string $node) => array(
'host' => $node,
'port' => $config->port,
'user' => '',
'password' => '',
'vhost' => $config->vhost
),
$config->nodes
)
);
$this->bxApp = $bxApp;
$this->ch = $this->conn->channel();
}
function publish(AMQPMessage $msg, string $exchange, string $routingKey): void {
$this->ch->basic_publish($msg, $exchange, $routingKey);
}
function consume(string $queue): ?AMQPMessage {
return $this->ch->basic_get($queue);
}
}
При создании или обновлении сущности на стороне сайта мы брали все нужные поля, упаковывали в JSON и отправляли в нужный exchange на стороне RabbitMQ. Соответственно, exchange были выделены по названиям сущностей, а само сообщение, для маршрутизации в нужную очередь, содержало routing_key с указанием отправителя.
На основании exchange и routing_key сообщение отправлялось в нужную очередь, которые, в свою очередь, были выделены по правилу <отправитель>_<сущность>.
При помощи opis/json-schema выполнялась валидация JSON-сообщений — как при получении, так и при отправке. Не бог весть что, признаться, но хотя бы можно было отсеивать гарантировано некорректные сообщения до валидации их в контексте бизнес-логики.
Получение реализовывали на агентах. Для незнакомых с Битриксом людей могу упомянуть, что это что-то вроде пользовательских функций, которые отрабатывают всем скопом на cron«e. По итогу, раз в минуту каждый сайт забирал из нужных ему очередей какое-то количество сообщений, валидировал их и выполнял необходимые действия с полученными данными.
consume();
if ($message === null) {
continue;
}
$messageBody = $message->getBody();
try {
$service->validator->validate($message->getBody(), $service->schema);
$entity = $service->toEntity($message);
$service->save($entity);
} catch (Exception $exception) {
$message->nack();
$logger->warning('...');
continue;
}
$message->ack();
} while ($message !== null && $messagesCount < Service::AGENT_LIMIT);
} catch (Throwable $t) {
$logger->error('...');
} finally {
return __METHOD__ . '();';
}
}
Оставалось еще две проблемы. Это контроль дубликатов и проставление связей между сущностями. Однако, как выяснилось, обе этих проблемы решаются вводом единого uuid. Главное, чтобы он был уникален для сущностей не в рамках одной системы, а нескольких. Поэтому идею с внутренними идентификаторами откинули и остановились на uuid4 и реализации от ramsey.
Вместе с полями сущности передавался так же ее uuid. При получении новых сообщений это помогало проверять наличие этой записи на целевом сайте. А в ситуациях, когда требовалось проставлять связи между сущностями (RabbitMQ не гарантирует порядка доставки сообщений в рамках одной очереди, и имели место быть ситуации, когда дочерняя сущность приходила раньше родительской), можно было в полях с привязками передавать uuid вместо внутренних идентификаторов.
Интеграция с Pradata
В Pradata у клиента хранились данные о продажах конечным клиентам. Естественно, они востребованы и уместны в CRM — менеджеры видят цифры реальных продаж в привязке к конкретным клиентам.
Для нас эта система была странным «чёрным ящиком».
Исходя из соображений безопасности, клиент не стал предоставлять нам доступ ко всей Pradata, ограничившись лишь формированием нескольких SQL VIEW с данными, которых, по его мнению, хватало нам для работы. И здесь мы столкнулись сразу с тремя проблемами:
Необходимо было подключить этот SQL VIEW к Битриксу. При этом, желательно, сохранив возможность использовать API Битрикса для работы с этими данными.
Клиент предоставил довольно небрежно сформированные данные. Таким образом, в некоторых таблицах можно было увидеть ~120 столбцов, среди которых были Номенклатура_ID, id_unique, id_unique2, attribute1 — attribute80. Да, там правда было 80 столбцов с соответствующими названиями. Как сейчас помню, в attribute4 хранился ИНН.
В одной из таблиц был подготовлен такой набор данных, что там просто физически не могло быть ничего, что можно считать первичным ключом. Даже вся строка целиком, в теории, могла быть не уникальной.
Как выяснилось, Битрикс отлично умеет работать с SQL VIEW, если подключить его как отдельную БД в файле настроек. Так что, указав в /bitrix/.setting.php новую конфигурацию, мы смогли подключиться к SQL VIEW без всяких проблем.
# bitrix/.settings.php
'connections' => array('value' => array(
'pradata' => array(
'className' => '\\Bitrix\\Main\\DB\\MssqlConnection',
'host' => 'view.client.ru',
'database' => 'db',
'login' => 'client',
'password' => '*********'
)
))
# usage
$c = \Bitrix\Main\Application::getConnection('pradata');
И это даже можно подружить с DataManager Битрикса, чтобы использовать ORM при работе с данными клиента.
configureColumnName('id_client')->configurePrimary(),
(new StringField('NAME'))->configureColumnName('tt_name'),
(new StringField('COUNTRY'))->configureColumnName('country'),
(new StringField('CITY'))->configureColumnName('city'),
(new StringField('PRADATA_ADDRESS'))->configureColumnName('tt_addres'),
(new StringField('INN'))->configureColumnName('attribute4'),
);
}
static function add(array $data): void {
throw new NotImplementedException('SQL VIEW is for reading data only.');
}
static function update($primary, array $data): void {
throw new NotImplementedException('SQL VIEW is for reading data only.');
}
static function delete($primary): void {
throw new NotImplementedException('SQL VIEW is for reading data only.');
}
}
Как видно, достаточно было определить метод getConnectionName (), чтобы все заработало.
Дополнительно, очень просто решилась проблема с именованием столбцов. Мы просто выбрали те, которые нам удобно использовать, после чего указали для каждого из них оригинальные, используя метод configureColumnName.
Имея на руках подобный класс, дальнейшее взаимодействие с этими данными оказалось очень простым и даже скучным.
А что про ту кривую таблицу без первичного ключа, спросите вы? А ничего. Не пригодилась нам она, по итогу.
Интеграция с учётной системой на .net
Учётная система одного из дистрибьюторов была написана на .net и находилась во внутреннем закрытом контуре. Данные из неё нужно было передать в CRM через промежуточную базу 1С. Для такой задачи уже точно никаких готовых модулей нет.
Помогло привлечение дружественного .net-разработчика и то, что в промежуточной базе данных мы использовали веб-сервисы с понятным механизмом вызова и составом данных. Разработчику оставалось написать небольшой коннектор, использующий наши веб-сервисы и внутренние методы учётной системы.
Основная задача адаптера — передать данные между 1С и внутренней учётной системой (назовём её ВУС). В ВУС данные попадают через корпоративную шину данных, которая располагается в закрытой среде. Чтобы иметь доступ к шине, адаптер должен разворачиваться в одной среде с ней, это накладывает ограничение доступа к самому адаптеру, он не может выступать в роли API, он может только сам запрашивать и передавать данные в 1С, используя разработанный нами ранее REST.
передача данных между 1С и ВУС
При написании кода адаптера мы учли, что он будет запущен в нескольких экземплярах для распределения нагрузки. Это накладывало риск повторного чтения данных из 1С. Пришлось организовать распараллеливание заданий между несколькими экземплярами.
Мы решили сделать один из запущенных экземпляров главным. Определение главного решается через общую таблицу в БД, в которую сами экземпляры вносят и обновляют информацию о себе, назначают себя главным, если такого нет. Задача такого экземпляра — забирать новые идентификаторы данных (ИД) от 1С API, для каждого ИД сформировать сообщение и положить в очередь на обработку.
Чтобы распределить нагрузку между экземплярами, обработку новых сообщений разделили на несколько этапов:
получение подробной информации по ИД от 1С,
формирование сообщения для учётной системы,
отправка и ожидание ответа ESB,
обработка и отправка в 1С успешного ответа,
обработка ошибки от ESB.
После окончания каждого этапа данные логируются и создается сообщение в очередь для следующего этапа.
Что нужно знать, интегрируя Битрикс24 с внешними системами
Вывод и итог одной строкой — Битрикс24 можно интегрировать практически с чем угодно. Вопрос только в цене и сроках.
Для простых случаев есть много решений «из коробки» (см. нашу таблицу). В сложных случаях понадобится разработчик со знанием платформы и прямыми руками.
Если вы ищете подрядчика на сложный интеграционный проект, лучше чтобы он мог строить сразу две половины моста. Например, брал на себя работы по интеграции и со стороны Битрикс24 и со стороны 1С. Это снижает риски и уменьшает ваши затраты на синхронизацию команд.
риски в сложном интеграционном проекте