Привет! Меня зовут Виктор, и у меня есть некоторый опыт в области телефонии и разработки. Хотел бы поделиться своим проектом — тепловой картой телефонных номеров. Хотя подобные карты уже существуют, аналогичной визуализации звонков на Хабре пока не встречал. Вот пример того, как она выглядит:

В настоящее время я работаю на одном из телеканалов. Передо мной поставили задачу создания тепловой карты входящих звонков клиентов на номера телеканала по всей территории Российской Федерации. В моём распоряжении находятся АТС Asterisk и соответствующие записи CDR (Call Detail Records), а также система мониторинга Grafana, тестовый сервер и две MySQL-базы данных.

Задача заключается в том, чтобы извлечь номера телефонов из CDR, определить их географическое местоположение и затем отобразить эти данные на карте, используя соответствующие координаты. Хочу сразу отметить, что DEF-номера не имеют прямой географической привязки, но в реестре российской системы нумерации такая информация всё же присутствует. Именно оттуда я загрузил CSV-файлы для импорта в базу данных.

Для получения координат (широта и долгота) мне пришлось воспользоваться API. Признаюсь, я обращался к платному ресурсу дадата, хотя они утверждают, что используют бесплатный источник информации с openstreetmap.org. Создаю запросы через API, извлекая уникальные строки из базы данных и добавляя координаты после получения ответа от сервиса:

<?php

// Настройки подключения к базе данных
$host = 'Адрес_базы';
$db   = 'Выбор_базы';
$user = 'Пользователь';
$pass = 'Пароль';
$charset = 'utf8mb4';

// Подключение к базе данных
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
    $pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
    throw new \PDOException($e->getMessage(), (int)$e->getCode());
}

// Функция для логирования
function log_message($message)
{
    // Сохраняем сообщение в лог-файл в директории /scripts
    $logFilePath = '/scripts/log.txt';

    // Выводим сообщение в консоль
    echo "$message\n";

    // Пишем сообщение в файл
    file_put_contents($logFilePath, "$message\n", FILE_APPEND | LOCK_EX);
}

// Чтение данных из таблицы
$sql = "SELECT DISTINCT region FROM asterisk.region";
$stmt = $pdo->query($sql);
$regions = $stmt->fetchAll(PDO::FETCH_COLUMN);

// Массив для хранения уникальных регионов
$uniqueRegions = [];

// Обрабатываем каждый уникальный регион
foreach ($regions as $region) {
    // Добавляем в массив уникальных регионов
    $uniqueRegions[] = ['region' => $region];

    // Логируем обработку региона
    log_message("Обрабатывается регион: $region");
}

// Ключ API
$apiToken = 'TOKEN';
$apiSecret = 'KEY';

// URL для обращения к API
$apiUrl = 'https://dadata.ru/api/address';

// Отправка запросов к API и получение координат
foreach ($uniqueRegions as $item) {
    sleep(1); // Пауза перед каждым запросом на 1 секунду

    // Данные для отправки
    $postData = json_encode([$item['region']]);

    // Инициализируем cURL
    $ch = curl_init();

    // Устанавливаем параметры cURL
    curl_setopt($ch, CURLOPT_URL, $apiUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        'Content-Type: application/json',
        'Accept: application/json',
        'Authorization: Token ' . $apiToken,
        'X-Secret: ' . $apiSecret
    ));
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);

    // Выполняем запрос
    $response = curl_exec($ch);

    // Проверяем, был ли получен ответ
    if ($response === false) {
        log_message("Не удалось получить координаты для региона {$item['region']} (ошибка HTTP).");
        continue;
    }

    // Закрываем соединение
    curl_close($ch);

    // Парсим JSON-ответ
    $data = json_decode($response, true);

    // Пример проверки наличия координат в новом формате ответа
    if (isset($data[0]['geo_lat'], $data[0]['geo_lon'])) {
        $latitude = $data[0]['geo_lat'];  // Здесь предполагаемый путь к широте
        $longitude = $data[0]['geo_lon']; // Здесь предполагаемый путь к долготе

        // Формируем строку для записи в БД
        $geo = "$latitude,$longitude";

        // Запись координат в базу данных
        $updateSql = "UPDATE asterisk.region SET geo = :geo WHERE region = :region";
        $stmt = $pdo->prepare($updateSql);
        $stmt->execute([':geo' => $geo, ':region' => $item['region']]);

        // Логируем успешную запись
        log_message("Записаны координаты для региона {$item['region']}: $geo");
    } else {
        // Логируем отсутствие координат
        log_message("Не удалось получить координаты для региона {$item['region']}");
    }
}

// Логируем завершение обработки
log_message("Обновление базы данных завершено.");

?>

В результате получилась вот такая картина, но некоторые координаты остались пустыми - около 106 000 строк не заполнились. Чтобы решить эту проблему, я вручную добавил координаты для крупных регионов, используя Яндекс.Карты и выполнив массовые обновления в базе данных.


Основная часть работы сделана - координаты получены. Теперь нужно взять номера из CDR, сопоставить их с префиксами и найти соответствующие координаты. Скрипт занял много времени, поэтому я решил упростить процесс и создал триггер прямо в базе данных. Возможно, это не самый правильный подход, но мне было важно ускорить работу. Этот триггер срабатывает каждый раз, когда появляется новая запись в CDR: номер парсится, и результат записывается в таблицу geo.

DELIMITER $$

CREATE TRIGGER trg_insert_geo_code
AFTER INSERT ON asterisk_cdr.cdr
FOR EACH ROW
BEGIN
    DECLARE v_region VARCHAR(255);
    DECLARE v_latitude DECIMAL(10,6);
    DECLARE v_longitude DECIMAL(10,6);
    
    -- Получаем данные для вставки в geo_codes
    SELECT r.region,
           SUBSTRING_INDEX(r.geo, ',', 1),
           SUBSTRING_INDEX(SUBSTRING_INDEX(r.geo, ',', -1), ',', 1)
    INTO v_region, v_latitude, v_longitude
    FROM asterisk_cdr.region r
    WHERE LEFT(NEW.src, 3) = r.prefix
      AND CAST(SUBSTR(NEW.src, 4) AS UNSIGNED) BETWEEN r.`from` AND r.`to`;
      
    -- Вставляем данные в geo_codes
    INSERT INTO asterisk_cdr.geo_codes (Name, Latitude, Longitude, record_id)
    VALUES (v_region, v_latitude, v_longitude, NEW.id);
END$$

DELIMITER ;
В итоге получаю такую вот табличку.
В итоге получаю такую вот табличку.

В grafana все стандартное и идет из коробки, просто добавил сервера для обращения и все.

Mysql_geo тот самый, на котором все и крутится, кроме графаны
Mysql_geo тот самый, на котором все и крутится, кроме графаны

Вот и всё - теперь есть красивая карта, которая автоматически обновляется и радует глаз.

Не судите строго, это первая публикация.

Комментарии (16)


  1. Max_2D
    02.02.2025 00:07

    Приветствую,как вариант можно в режиме реального времени получать номер с помощью правок в dialplan как вариант.


  1. mlnw
    02.02.2025 00:07

    В эпоху, когда у людей обычно один номер на всю жизнь, но меняются и операторы, и города, и регионы обитания, думать, что по DEF-коду что-то можно определить - наивно. А когда так думает человек, подписывающийся как "инженер телефонист" - то это по меньшей мере странно.


    1. temadiary
      02.02.2025 00:07

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


    1. vadimk91
      02.02.2025 00:07

      Как человек, работающий в связи уже 30+ лет, скажу - геопривязка однозначно работала и работает для номеров стационарных телефонов, для мобильных не так однозначно. У нас (райцентр, СЗФО) примерно 80% клиентов обращаются с мобильных номеров, которые изначально были розданы операторами регионам, но оставшаяся - тут полное разнообразие, иногда такой код встречаешь вообще впервые.


      1. alex_ak1
        02.02.2025 00:07

        А в момент звонка оператор имеет информацию о том, откуда пришел звонок? Наверняка есть оператор, но знает ли он с какой вышки (к примеру) идёт голос?


        1. vadimk91
          02.02.2025 00:07

          нет, полные данные есть только у того оператора, чья базовая станция обслуживает исходящий вызов


      1. mlnw
        02.02.2025 00:07

        геопривязка однозначно работала и работает для номеров стационарных телефонов

        В эпоху voip и коллцентров, работающих из одного места на всю страну, подставляющих callerid каждого звонка def-кодом целевого региона, уже нет.


    1. ErshoFF
      02.02.2025 00:07

      Практически на данный момент DEF код является географическим (оператору дается код для использования в конкретном регионе) - так что авто при нанесении на карту согласно DEF коду близок к истине.
      Перенос номер между региона находится на стадии проработки, дата включения возможности переноса не установлена.


      1. mlnw
        02.02.2025 00:07

        Если вы про MNP, то ввиду повсеместной отмены внутресетевого роуминга перенос потерял былую актуальность.

        А после появления VoWiFi люди продолжают пользоваться российскими номерами, работая напостоянку из заграницы.


        1. Andreyika
          02.02.2025 00:07

          Внутрисетевой роуминг последние лет 15 особо и не был проблемой - цена вопроса 1$ (30руб) в месяц и вы "ну прям как в домашнем регионе". Проблема начинается, когда вам позвонить попытаются за 4рубля/минута вместо изпакета. Ну и стоимость тарифа в челябинске и мск может на 30-40% отличаться запросто (не в пользу мск)


    1. Lpndn
      02.02.2025 00:07

      def'ы ещё не могут переезжать между регионами, так что нормально


      1. MarksMan09
        02.02.2025 00:07

        А людям с этими номерами то что переезжать мешает - крепостное право? :)


        1. fujikiriku
          02.02.2025 00:07

          Ну что вы такие старые вещи вспоминаете, нет, крепостное право уже отменили, хотя многие считают что зря.. нет, сейчас обычный визовый режим


    1. tortor
      02.02.2025 00:07

      На практике, по крайней мере еще в прошлом году, не удавалось при смене региона забрать свой номер "с собой" как внутри одного оператора (в моем случае - МТС), так и при переходе к другому по MNP. Мол, хотите переехать с своим номером - извольте, да только номер должен быть "нашего" региона. Так что номер на практике имеет все-таки ограниченную мобильность даже внутри страны. Ну а то, что сам человек, этот номер использующий, в моменте может путешествовать хоть по всему миру - это понятно. Роуминг работал еще и в 90-е.


  1. orland
    02.02.2025 00:07

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


  1. VictorSkvorez Автор
    02.02.2025 00:07

    Ребята, здорово, что я получил столько откликов на свою работу! Вы абсолютно правы насчёт геопозиции DEF-номера – да, это условная информация. Я, конечно, не претендую на «Оскар» от Хабра. Хотя в телекоме я работаю уже довольно давно, но это не столь важно. Главное, что даже если я попал в цель хотя бы наполовину, для меня и компании этого вполне достаточно. Эта реализация – скорее красивая иллюстрация, чем строгий статистический анализ. Конечно, я осознаю, что точную геолокацию DEF-номера может определить только оператор через триангуляцию, но доступа к таким данным у меня нет. Кстати, если говорить об ABC-нумерации, ничто не мешает мне, например, разместить сервер в Амстердаме, арендовать питерский номер и позвонить из Уфы – в эпоху VoIP это несложно организовать. Спасибо вам за обратную связь и карму, очень приятно!