Реалізація Real-Time трекінгу кур'єра/замовлення на сайті
Користувач оформив замовлення і чекає на кур'єра. Сторінка «Мої замовлення» з полем «статус: в дорозі» — це вже минуле століття. Сучасний стандарт — карта з живим маркером кур'єра та лічильником «прибуде через N хвилин». Технічно це зв'язка трьох компонентів: мобільний додаток/пристрій кур'єра, бекенд-додаток, клієнтський браузер. Ми побудували такі системи для 30+ проектів доставки — від локальних сервісів до федеральних мереж. Пропонуємо рішення під ключ: від 4 днів на базовий трекінг. Зв'яжіться з нами для розрахунку вашого проекту.
Чому 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-з'єднань при тисячах кур'єрів, дотримання приватності каналів та плавну анімацію без ривків. Замовте оцінку вашого проекту — запропонуємо оптимальне рішення.







