Привет, Хабр! Одна из ключевых задач в области маркетинга производительности — это понять, какая реклама действительно ведет клиентов. Существует yandex.metrica для кликов, но когда одно из целевых действий — это вызов, сложнее проанализировать источники, что означает понимание того, какие творческие работают лучше.
Использование динамических виртуальных номеров позволяет сравнивать вызовы с конкретным источником рекламы. Для каждого посещения система заменяет уникальный номер телефона и исправляет звонок. Когда клиент звонит, мы подключаем его призыв к конкретной рекламе.
В этой статье мы проанализируем:
-
Общая рабочая диаграмма
-
Как реализовать его на php yii2
-
Давайте отобразим код с архитектурой
-
Мы обсудим проблемы и способы развития
Содержание
Общая рабочая диаграмма
-
Пользователь нажимает на рекламу и прибывает на странице назначения с тегами UTM
-
На странице номер телефона пула ваших бесплатных номеров в MTS Exolve динамически заменяется
-
Посещение сохраняется в базе данных с номером, тегами UTM и настройками браузера
-
Номер зарезервирован для пользователя на некоторое время, например, на 1 час
-
Если в течение этого времени они вызовут номер, MTS EXOLE отправит WebHuk
-
Мы исправляем звонок и подключаем его при посещении
-
Если не было звонка, опубликован номер
Архитектурные решения
Для удобства поддержки и масштабирования мы разделяем систему на четыре уровня:
-
Контроллер — принимает запросы с передней части, например, поддержав посещение
-
Сервис оспаривает коммерческую логику, такую как бронирование номера и запись звонка
-
Горкость работает только с базой данных, не содержит бизнес -логики
-
Заказчик — отдельный класс для взаимодействия с внешним внешним API
Этот подход позволяет вам оставлять «тонкие» контроллеры, практично протестировать бизнес -логику и изменять интеграцию с внешними службами без изменения всего приложения.
Таблицы базы данных
Для демонстрации мы используем три основных объекта:
-
Телефон — пул виртуальных номеров;
-
Посещения посещений, включая номер и теги UTM;
-
Вызов — звонки, связанные с посещениями.
Создание таблиц и соединений
FAйL: M250913_135055_CREATE_DYNAMIC_NUMBERS_TABLE.PHP
Создайте телефон, посетите и звоните, добавляет подсказки и соединения между таблицами.
createTable('{{%phone}}', [
'id' => $this->primaryKey(),
'number' => $this->string(20)->notNull()->unique(),
'status' => $this->string(20)->notNull(),
'created_at' => $this->dateTime()->defaultExpression('NOW()'),
'updated_at' => $this->dateTime()->append('ON UPDATE NOW()'),
]);
$this->createTable('{{%visit}}', [
'id' => $this->primaryKey(),
'phone_number' => $this->string(20)->notNull(),
'utm_source' => $this->string(50),
'utm_campaign' => $this->string(50),
'utm_medium' => $this->string(50),
'ip' => $this->string(45),
'user_agent' => $this->text(),
'created_at' => $this->dateTime()->defaultExpression('NOW()'),
'updated_at' => $this->dateTime()->append('ON UPDATE NOW()'),
]);
$this->createTable('{{%call}}', [
'id' => $this->primaryKey(),
'call_id' => $this->string(50)->notNull()->unique(),
'phone_number' => $this->string(20)->notNull(),
'visit_id' => $this->integer(),
'created_at' => $this->dateTime()->defaultExpression('NOW()'),
'updated_at' => $this->dateTime()->append('ON UPDATE NOW()'),
]);
$this->createIndex('idx-visit-phone_number', '{{%visit}}', 'phone_number');
$this->createIndex('idx-call-phone_number', '{{%call}}', 'phone_number');
$this->addForeignKey('fk-call-visit_id', '{{%call}}', 'visit_id', '{{%visit}}', 'id', 'SET NULL', 'CASCADE');
}
public function safeDown()
{
$this->dropForeignKey('fk-call-visit_id', '{{%call}}');
$this->dropTable('{{%call}}');
$this->dropTable('{{%visit}}');
$this->dropTable('{{%phone}}');
}
}
Ссылки и услуги
Здесь мы проанализируем ключевые части системы, которые позволят вам управлять данными и логикой службы, чтобы все работало стабильным и прозрачным образом.
Управление фигурами
Файл: app / restority / phonepository.php отвечает за управление пулом номеров. Здесь, операции поиска первой бесплатной проблемы, его резерв и его выпуск сосредоточены. Репозиторий гарантирует, что система не будет работать с «грязными» данными, что гарантирует точность состояния телефонов.
getIsNewRecord()) {
throw new RuntimeException('Adding existing model.');
}
if (!$model->insert(false)) {
throw new RuntimeException('Saving error.');
}
}
public function findFree(): ?Phone
{
return Phone::find()
->where(['status' => Phone::STATUS_FREE])
->orderBy(['updated_at' => SORT_ASC]) // самые старые свободные номера первыми
->one();
}
public function markReserved(int $id): void
{
Phone::updateAll(['status' => Phone::STATUS_RESERVED], ['id' => $id]);
}
public function markFree(string $number): void
{
Phone::updateAll(['status' => Phone::STATUS_FREE], ['number' => $number]);
}
}
Коммерческая логика чисел
Файл: App / Service / phoneservice.php добавляет бизнес к операциям по номерам. Если PhonePository просто ищет телефон, сервис немедленно передает его в статус «занятой». Это исключает ситуации, когда тот же номер случайно исправляет для двух пользователей.
db->beginTransaction();
try {
$phone = $this->phoneRepository->findFree();
if (!$phone) {
$transaction->rollBack();
return null;
}
$this->phoneRepository->markReserved($phone->id);
$transaction->commit();
return $phone->number;
} catch (Exception $e) {
$transaction->rollBack();
throw $e;
}
}
public function release(string $number): void
{
$this->phoneRepository->markFree($number);
}
}
Посетите и поиск по номеру
Файл: App / RESTITOR / VISTORPOSITORION.PHP. Он сохраняет посещения пользователей с их атрибутами: IP -адреса, теги UTM, браузер, телефон. Кроме того, именно он знает, как найти посещение номера телефона, который становится ключом при обработке звонка.
getIsNewRecord()) {
throw new RuntimeException('Adding existing model.');
}
if (!$model->insert(false)) {
throw new RuntimeException('Saving error.');
}
}
public function findByPhoneWithinHour(string $number): ?Visit
{
$threshold = date('Y-m-d H:i:s', time() - 3600);
return Visit::find()
->where(['phone_number' => $number])
->andWhere(['>=', 'created_at', $threshold])
->one();
}
}
Проверьте посещение визита
Файл: app / forms / vistform.php обеспечивает проверку ввода при создании посещения: IP, счетчик UTM и браузер. Таким образом, бизнес -логика уже получает гарантированные правильные данные, и любая проверка сосредоточена в одном месте.
utm_source = $visit->utm_source;
$this->utm_medium = $visit->utm_medium;
$this->utm_campaign = $visit->utm_campaign;
$this->ip = $visit->ip;
$this->user_agent = $visit->user_agent;
}
parent::__construct($config);
}
public function rules()
{
return [
[['ip'], 'ip'],
[['utm_source', 'utm_medium', 'utm_campaign', 'user_agent'], 'string'],
];
}
}
Создайте посещение и забронируйте номер
Файл: App / Service / VISTITSERVICE.PHP управляет процессом создания посещения. Он принимает входные данные контроллера (IP, UTM, браузер), запрашивает номер на Phonesservice, а затем поддерживает посещение через визитпорозири.
phoneService->reserve();
if (!$phone) throw new \DomainException('Нет свободных номеров');
$entity = Visit::create(
$phone,
$form->utm_source,
$form->utm_medium,
$form->utm_campaign,
$form->ip,
$form->user_agent,
);
$this->visitRepository->add($entity);
return $entity;
}
}
Обработка запроса на посещение
Файл: App / Controllers / vitionController.php здесь принимает меры предосторожности Ajax на сайте. Контроллер проверяет входящие данные через форму визита, транзит с посещениями и возвращает результат в формат JSON. Это может быть номер телефона или ошибка в случае неправильных данных. Контроллер выполняет только функцию «Я получил → Проверено → I передал больше → дал ответ».
response->format = Response::FORMAT_JSON;
$request = json_decode(Yii::$app->request->getRawBody(), true);
$form = new VisitForm();
if ($form->load($request) && $form->validate()) {
try {
$form->ip = Yii::$app->request->userIP;
$visit = $this->service->create($form);
return $this->asJson(['phone' => $visit->phone_number]);
} catch (\DomainException $e) {
Yii::$app->errorHandler->logException($e);
return $this->asJson(['error' => $e->getMessage()]);
}
}
return $this->asJson(['error' => 'Invalid data']);
}
}
Звоните в хранилище данных
Файл: App / Restitory / callrepository.php хранит хранилище. Его задача состоит в том, чтобы поддерживать новые вызовы и отслеживать уникальность call_id. Благодаря этому система защищена от двойных данных, например, с повторным уведомлением от телефонии.
getIsNewRecord()) {
throw new RuntimeException('Adding existing model.');
}
if (!$model->insert(false)) {
throw new RuntimeException('Saving error.');
}
}
public function findByCallId(string $callId): bool
{
return Call::find()
->where(['call_id' => $callId])
->exists();
}
}
Привязан к визиту
Файл: App / Service / Callservice Applications.php. Он получает данные от телефона через ExoLeclient, определяет количество, по которому был звонок, и находит подходящее посещение через посетителя. После этого он захватывает вызов в базе данных, используя CallRepository. Таким образом, звонок является частью награды и связан с конкретным визитом.
callRepository->findByCallId($callId)) {
return false;
}
$data = $this->exolveClient->getInfo($callId);
$number = $data['to'] ?? null;
if (!$number || !is_string($number)) {
return false;
}
$visit = $this->visitRepository->findByPhoneWithinHour($number);
$entity = Call::create(
$callId,
$number,
$visit?->id,
);
$db = Yii::$app->db;
return $db->transaction(function () use ($entity, $number) {
$this->callRepository->add($entity);
$this->phoneService->release($number);
return true;
});
}
}
Интеграция с Exolve MTS
Файл: App / Customer / Exol delvet.php затрагивает работу с API телефона. Основным методом является getInfo, который используется для получения информации о вызовах. Этот класс отправляет запросы в API, получает звонки, проверяет правильный ответ и возвращает результат в практическом формате для услуг.
httpClient = new Client(['baseUrl' => self::ENDPOINT]);
$this->apiKey = Yii::$app->params['exolve']['apiKey'] ?? '';
}
/**
* Получение информации о звонке по call_id
*
* @param string $callId
* @return ?array
* @throws Exception
*/
public function getInfo(string $callId): ?array
{
try {
$response = $this->httpClient->post(
'/statistics/call-history/v2/GetInfo',
['call_id' => [$callId]]
)
->addHeaders(['Authorization' => "Bearer {$this->apiKey}"])
->setFormat(Client::FORMAT_JSON)
->send();
if (!$response->isOk) {
\Yii::error("Ошибка Exolve API: {$response->content}", __METHOD__);
return null;
}
return $response->data ?? null;
} catch (\Throwable $e) {
\Yii::error("Сбой при обращении к Exolve: {$e->getMessage()}", __METHOD__);
return null;
}
}
}
Часть клиента на JavaScript
Для правильного приписывания вызова конкретного посещения и рекламной кампании этот код собирает посещение пользователя на сайт метки в качестве тегов UTM и браузера и автоматически определяет желаемый номер телефона по странице.
function getUTM() {
const params = new URLSearchParams(window.location.search);
return {
utm_source: params.get('utm_source'),
utm_medium: params.get('utm_medium'),
utm_campaign: params.get('utm_campaign')
};
}
fetch('/visit/create', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
VisitForm: {
user_agent: navigator.userAgent,
...getUTM()
}
})
}).then(r => r.json()).then(data => {
if (data.phone) {
document.getElementById('phone').innerText = data.phone;
}
if (data.error) {
console.error('Ошибка при создании визита:', data.error);
}
});
Заключение
Система динамических номеров позволяет точно сравнивать вызовы с источниками рекламы. Числа заменяются автоматически, посещения фиксируются без ошибок, и резервирование номера в течение определенного времени для посетителя гарантирует правильное соединение вызовов с посещением.
Важно принять во внимание нюанс — после окончания периода бронирования число опубликовано и может быть назначено другому пользователю. Чтобы снизить риск перекрестков, логично расширить пул чисел или увеличить время для их удержания.
В этом примере представлены только ключевые компоненты и общая архитектура системы. В кодовой базе нет телефонов, посещения и звонков, а также логики количества номеров в отсутствие вызова. Для полного развертывания вам нужно будет установить YII2, настроить зависимости и разработку этих модулей. Если вы хотите собрать полную систему, напишите в комментариях и, в следующем оборудовании, мы проанализируем реализацию более подробно.
Идеи развития
-
Добавьте усовершенствованный браузер и настройки географии.
-
Строите кампании в Telegram или Grafana.
-
Когда атрибуты учитывают повторные посещения.
-
Интегрируйтесь с CRM.
-
Получите автоматически ExoLish MTS Foam Pool.
-
Автоматически покупать виртуальные номера для значительного трафика.
-
Когда вы звоните, направляя конверсию в систему прямой рекламы VK или Yandex.