Реализация Real-Time трекинга курьера/заказа на сайте
Пользователь оформил заказ и ждёт курьера. Страница «Мои заказы» с полем «статус: в пути» — это уже прошлый век. Современный стандарт — карта с живым маркером курьера и счётчиком «прибудет через N минут». Технически это связка трёх компонентов: мобильное приложение/устройство курьера, бэкенд-приложение, клиентский браузер. Мы построили такие системы для 30+ проектов доставки — от локальных сервисов до федеральных сетей. Свяжитесь с нами для расчёта вашего проекта.
Почему polling базы — плохой выбор?
Казалось бы, можно опрашивать сервер каждые 5 секунд: GET /api/order/status. Но это создаёт лишнюю нагрузку на сервер, увеличивает TTFB для остальных запросов и не даёт мгновенного обновления. WebSocket в разы эффективнее: событие приходит сразу, без задержки опроса. Однако простая реализация WebSocket без авторизации открывает доступ к чужим заказам. Мы используем PrivateChannel Laravel Broadcasting с проверкой прав.
Архитектура потока данных
[Устройство курьера] GPS → POST /api/courier/location каждые 3–5с ↓ [Backend] Сохранить в Redis (TTL 60s) Publish в Redis Pub/Sub канал order:{id} ↓ [WebSocket сервер (Laravel Reverb / Pusher)] Broadcast event LocationUpdated ↓ [Браузер клиента] Обновить маркер на карте Геопозиции не хранятся в PostgreSQL при каждом обновлении — это 720 записей в час на одного курьера. В базу пишем только при смене статуса заказа и финальную позицию при завершении. Текущая позиция — в Redis с TTL.
Почему Redis — оптимальное хранилище для геоданных?
Redis обеспечивает низкую задержку записи и чтения (субмиллисекунды) и автоматическое удаление устаревших данных через TTL. Для трекинга это критично: позиция курьера актуальна лишь короткое время. Постоянная запись в PostgreSQL привела бы к 720 записям в час на одного курьера — неоправданная нагрузка на базу. С Redis мы экономим ресурсы и ускоряем чтение: получить текущую позицию можно за один запрос к in-memory хранилищу. Экономия на серверной инфраструктуре может достигать 40% за счёт отказа от постоянного polling.
Пример конфигурации WebSocket-сервера с Laravel Reverb
Для продакшена рекомендуется использовать Reverb с горизонтальным масштабированием через Redis. Настройка сводится к установке пакета, публикации конфига и запуску воркера. Подробнее — в официальной документации Laravel.Таблица заказов
CREATE TABLE delivery_orders ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), courier_id BIGINT REFERENCES couriers(id), status VARCHAR(50) NOT NULL DEFAULT 'pending', -- pending | assigned | picked_up | in_transit | delivered | failed address_lat DECIMAL(10, 8), address_lng DECIMAL(11, 8), address_text VARCHAR(500), estimated_at TIMESTAMP, delivered_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT NOW() ); CREATE TABLE delivery_status_log ( id BIGSERIAL PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES delivery_orders(id), status VARCHAR(50) NOT NULL, lat DECIMAL(10, 8), lng DECIMAL(11, 8), note TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW() ); API курьера: обновление позиции и broadcast
class CourierLocationController extends Controller { public function update(Request $request, DeliveryOrder $order): JsonResponse { $data = $request->validate([ 'lat' => 'required|numeric|between:-90,90', 'lng' => 'required|numeric|between:-180,180', ]); $key = "courier_location:{$order->courier_id}"; Redis::setex($key, 60, json_encode([ 'lat' => $data['lat'], 'lng' => $data['lng'], 'order_id' => $order->id, 'ts' => now()->timestamp, ])); broadcast(new CourierLocationUpdated( orderId: $order->id, lat: $data['lat'], lng: $data['lng'], eta: $this->calculateEta($order, $data['lat'], $data['lng']), )); return response()->json(['ok' => true]); } private function calculateEta(DeliveryOrder $order, float $lat, float $lng): ?int { $distanceKm = $this->haversineKm($lat, $lng, $order->address_lat, $order->address_lng); return (int) round($distanceKm / 30 * 60); } } // CourierLocationUpdated event class CourierLocationUpdated implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; public function __construct( public readonly int $orderId, public readonly float $lat, public readonly float $lng, public readonly ?int $eta, ) {} public function broadcastOn(): Channel { return new PrivateChannel("order.{$this->orderId}"); } public function broadcastWith(): array { return [ 'lat' => $this->lat, 'lng' => $this->lng, 'eta' => $this->eta, ]; } } Авторизация канала в routes/channels.php:
Broadcast::channel('order.{orderId}', function (User $user, int $orderId) { return $user->id === DeliveryOrder::find($orderId)?->user_id; }); Канал PrivateChannel гарантирует, что только владелец заказа получает координаты. Это обязательное требование безопасности.
Клиентская часть: карта и анимация маркера
Используем Mapbox для отображения карты. Альтернатива — Яндекс.Карты, предпочтительнее для СНГ.
import mapboxgl from 'mapbox-gl'; // ... инициализация карты Echo.private(`order.${orderId}`) .listen('CourierLocationUpdated', ({ lat, lng, eta }) => { animateMarker(courierMarker, courierMarker.getLngLat().toArray(), [lng, lat]); if (eta !== null) { document.getElementById('eta').textContent = eta < 2 ? 'Курьер уже рядом' : `Прибудет через ~${eta} мин`; } }); function animateMarker(marker, from, to, duration = 500) { const start = performance.now(); function step(now) { const t = Math.min((now - start) / duration, 1); const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; const lng = from[0] + (to[0] - from[0]) * ease; const lat = from[1] + (to[1] - from[1]) * ease; marker.setLngLat([lng, lat]); if (t < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } Сравнение методов расчёта ETA
| Метод | Точность | Время построения маршрута | Зависимости |
|---|---|---|---|
| По прямой (Haversine) | Низкая (игнорирует дороги) | Мгновенно | Нет |
| Routing API (OSRM) | Высокая (учитывает дороги) | 50–200 мс | Внешний сервис |
| Google Directions API | Очень высокая (трафик) | 200–500 мс | API-ключ |
Сравнение подходов к real-time доставке
| Подход | Задержка обновлений | Нагрузка на сервер | Сложность реализации |
|---|---|---|---|
| Polling (HTTP каждые 5 с) | 5 сек в среднем | Высокая | Низкая |
| WebSocket (постоянное соединение) | Мгновенно | Низкая | Средняя |
| Server-Sent Events | Мгновенно | Низкая | Средняя |
Как настроить Private Channel за 5 шагов
- Установите Laravel Reverb или Pusher.
- Настройте broadcasting в config/broadcasting.php.
- Создайте Event, реализующий ShouldBroadcast.
- Определите PrivateChannel в routes/channels.php.
- Подпишитесь на канал на клиенте через Echo.
Что входит в результат
- Аналитика: интеграция с мобильным приложением курьера, согласование протокола.
- Проектирование: схема данных, WebSocket-архитектура, авторизация.
- Реализация: бэкенд на Laravel с Redis, broadcast, карта на фронтенде.
- Тестирование: нагрузочное тестирование WebSocket (1k+ одновременных подключений).
- Документация: описание API, инструкция по деплою.
- Поддержка: гарантия на код, обучение команды.
Сроки ориентировочно
- Базовый трекинг (Redis + broadcast + карта): от 4 до 5 дней.
- Авторизация Private Channel + логика доступа: от 1 дня.
- ETA-расчёт по прямой (Haversine): от 0.5 дня.
- ETA через Routing API: от 1 до 2 дней.
- Анимация маркера + сглаживание пути: от 1 дня.
- Push-уведомления при смене статуса: от 1 до 2 дней.
- Административная панель диспетчера: от 3 до 4 дней.
Почему стоит заказать у нас
Мы реализовали real-time трекинг для 30+ проектов, включая крупные службы доставки. Гарантируем стабильную работу WebSocket-соединений при тысячах курьеров, соблюдение приватности каналов и плавную анимацию без рывков. Закажите оценку вашего проекта — предложим оптимальное решение.







