Привет, Хабр! Одна из ключевых задач в области маркетинга производительности — это понять, какая реклама действительно ведет клиентов. Существует yandex.metrica для кликов, но когда одно из целевых действий — это вызов, сложнее проанализировать источники, что означает понимание того, какие творческие работают лучше.

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

В этой статье мы проанализируем:

  • Общая рабочая диаграмма

  • Как реализовать его на php yii2

  • Давайте отобразим код с архитектурой

  • Мы обсудим проблемы и способы развития

Общая рабочая диаграмма

  1. Пользователь нажимает на рекламу и прибывает на странице назначения с тегами UTM

  2. На странице номер телефона пула ваших бесплатных номеров в MTS Exolve динамически заменяется

  3. Посещение сохраняется в базе данных с номером, тегами UTM и настройками браузера

  4. Номер зарезервирован для пользователя на некоторое время, например, на 1 час

  5. Если в течение этого времени они вызовут номер, MTS EXOLE отправит WebHuk

  6. Мы исправляем звонок и подключаем его при посещении

  7. Если не было звонка, опубликован номер

Архитектурные решения

Для удобства поддержки и масштабирования мы разделяем систему на четыре уровня:

  • Контроллер — принимает запросы с передней части, например, поддержав посещение

  • Сервис оспаривает коммерческую логику, такую ​​как бронирование номера и запись звонка

  • Горкость работает только с базой данных, не содержит бизнес -логики

  • Заказчик — отдельный класс для взаимодействия с внешним внешним API

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

Таблицы базы данных

Для демонстрации мы используем три основных объекта:

  • Телефон — пул виртуальных номеров;

  • Посещения посещений, включая номер и теги UTM;

  • Вызов — звонки, связанные с посещениями.

ЧИТАТЬ  Обновлены структурированные данные Google Forum с упором на авторство.

Создание таблиц и соединений

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.

Source