Разработка системы выбора пункта выдачи на карте для интернет-магазина

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка системы выбора пункта выдачи на карте для интернет-магазина
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка системы выбора пункта выдачи на карте для интернет-магазина

Покупатель открывает шаг доставки, видит список из 47 адресов пунктов выдачи, пытается найти нужный район в выпадающем списке и закрывает вкладку. Карта с маркерами решает эту проблему: человек видит точки рядом с домой, работой, по дороге — и выбирает за 5 секунд.

Источники данных о ПВЗ

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

СДЭК:

GET https://api.cdek.ru/v2/deliverypoints?city_code=44&weight_max=30&type=PVZ
Authorization: Bearer {token}

Ответ содержит массив entity с полями location.latitude, location.longitude, work_time, address_comment, allowed_max_weight.

Boxberry:

GET https://api.boxberry.ru/json.php?token={token}&method=ListPoints&CityCode=77&prepaid=1

Структура другая, но данные те же — координаты, адрес, режим работы.

Кеширование справочника ПВЗ

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

// Artisan command: php artisan delivery:sync-pickup-points
class SyncPickupPoints extends Command
{
    public function handle(CdekService $cdek, BoxberryService $boxberry): void
    {
        $carriers = [
            'cdek'     => fn() => $cdek->getAllPickupPoints(),
            'boxberry' => fn() => $boxberry->getAllPickupPoints(),
        ];

        foreach ($carriers as $carrier => $fetcher) {
            $points = $fetcher();
            $this->info("$carrier: {$points->count()} points");

            PickupPoint::where('carrier', $carrier)->delete();

            PickupPoint::insert(
                $points->map(fn($p) => [
                    'carrier'      => $carrier,
                    'external_id'  => $p['code'],
                    'name'         => $p['name'],
                    'address'      => $p['address'],
                    'city'         => $p['city'],
                    'lat'          => $p['lat'],
                    'lng'          => $p['lng'],
                    'work_time'    => $p['work_time'],
                    'max_weight'   => $p['max_weight_kg'],
                    'cash_allowed' => $p['cash_allowed'],
                    'updated_at'   => now(),
                ])->toArray()
            );
        }

        $this->info('Done');
    }
}

Команда запускается через cron ночью. Пользователь получает данные из локальной БД за 10–20 мс вместо 500–2000 мс от API.

Геопространственные запросы

После того как покупатель вводит свой адрес или делится геолокацией, нужно показать ближайшие ПВЗ. PostGIS делает это элегантно:

-- Включение расширения (один раз)
CREATE EXTENSION IF NOT EXISTS postgis;

-- Добавление geography-колонки
ALTER TABLE pickup_points ADD COLUMN location geography(POINT, 4326);
UPDATE pickup_points SET location = ST_SetSRID(ST_MakePoint(lng, lat), 4326);
CREATE INDEX idx_pickup_points_location ON pickup_points USING GIST(location);

-- Запрос: 20 ближайших ПВЗ в радиусе 10 км, несущих грузы до 5 кг
SELECT
    id, carrier, name, address, work_time, cash_allowed,
    ST_Distance(location, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)) AS distance_m
FROM pickup_points
WHERE
    max_weight >= :weight
    AND ST_DWithin(
        location,
        ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography,
        10000
    )
ORDER BY distance_m
LIMIT 20;

Без PostGIS можно использовать формулу гаверсинуса прямо в SQL или в PHP — но это медленнее и менее точно.

Карта: рендеринг маркеров

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

import L from 'leaflet';
import 'leaflet.markercluster';

const map = L.map('pickup-map').setView([55.7558, 37.6173], 11);

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);

const markers = L.markerClusterGroup({
  maxClusterRadius: 50,
  iconCreateFunction: (cluster) => {
    const count = cluster.getChildCount();
    return L.divIcon({
      html: `<div class="cluster-icon">${count}</div>`,
      className: '',
      iconSize: [40, 40],
    });
  },
});

// Загрузка точек с учётом текущего viewport карты
map.on('moveend', async () => {
  const bounds = map.getBounds();
  const response = await fetch('/api/pickup-points?' + new URLSearchParams({
    north: bounds.getNorth(),
    south: bounds.getSouth(),
    east:  bounds.getEast(),
    west:  bounds.getWest(),
    weight: cartWeight,
  }));
  const points = await response.json();

  markers.clearLayers();
  points.forEach((point) => {
    const marker = L.marker([point.lat, point.lng], {
      icon: carrierIcon(point.carrier),
    });
    marker.bindPopup(buildPopup(point));
    marker.on('click', () => selectPickupPoint(point));
    markers.addLayer(marker);
  });
});

map.addLayer(markers);

Загрузка только видимой области (moveend) — вместо выгрузки всех точек сразу. Для 50 000 точек по всей России это принципиально.

Попап с деталями ПВЗ

function buildPopup(point) {
  return `
    <div class="pickup-popup">
      <div class="carrier-badge ${point.carrier}">${point.carrier.toUpperCase()}</div>
      <strong>${point.name}</strong>
      <p>${point.address}</p>
      <p class="work-time">${point.work_time}</p>
      ${point.cash_allowed ? '<span class="badge">Наличные</span>' : ''}
      <p class="delivery-cost">Доставка: <b>${formatPrice(point.cost)} ₽</b></p>
      <p class="delivery-days">Срок: ${point.min_days}–${point.max_days} дн.</p>
      <button onclick="selectPickupPoint(${point.id})">Выбрать</button>
    </div>
  `;
}

Определение города покупателя

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

async function detectUserLocation() {
  // Попытка через IP-геолокацию
  try {
    const res = await fetch('https://ipapi.co/json/');
    const data = await res.json();
    return { city: data.city, lat: data.latitude, lng: data.longitude };
  } catch {
    return { city: 'Москва', lat: 55.7558, lng: 37.6173 };
  }
}

ipapi.co даёт 1000 бесплатных запросов в сутки. Для больших магазинов — собственная база GeoIP (MaxMind GeoLite2, бесплатная).

Фильтрация ПВЗ

Если покупатель хочет только ПВЗ с примеркой (для одежды), или только постаматы (работают ночью), или только те, где принимают наличные — нужны фильтры:

const filters = {
  fitting_room: false,
  cash_allowed: false,
  type: 'all', // 'pvz' | 'postamat' | 'all'
  carrier: 'all',
};

// При изменении фильтров — перезапросить точки
Object.keys(filters).forEach((key) => {
  document.getElementById(`filter-${key}`).addEventListener('change', (e) => {
    filters[key] = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
    loadPickupPoints();
  });
});

Подтверждение выбора и передача в заказ

После выбора ПВЗ — его данные попадают в форму заказа. ID пункта выдачи хранится как часть способа доставки:

function selectPickupPoint(point) {
  selectedPoint = point;

  // Обновляем UI
  document.getElementById('selected-point-address').textContent = point.address;
  document.getElementById('selected-point-work-time').textContent = point.work_time;

  // Передаём в форму заказа
  document.getElementById('delivery_type').value = 'pickup';
  document.getElementById('pickup_point_id').value = point.id;
  document.getElementById('pickup_carrier').value = point.carrier;
  document.getElementById('pickup_external_id').value = point.external_id;
  document.getElementById('delivery_cost').value = point.cost;
}

Сроки разработки

Карта с ПВЗ одной службы, данные из API с кешем, кластеризация маркеров — 4–6 дней. Агрегатор нескольких служб, геолокация, фильтры — 2 недели. Добавление ПВЗ на карту к существующей форме заказа без переработки архитектуры — 3–5 дней.