Архитектура отслеживания заказов: два канала данных
Клиент открывает приложение через 40 минут после оформления заказа и видит статус «В обработке» — тот же, что был сразу после оплаты. Звонит в поддержку. Проблема не в логистике: курьер уже едет, координаты обновляются на сервере каждые 30 секунд. Проблема в том, что мобильное приложение не подключено к этому потоку. Мы сталкивались с этим в 80% проектов на старте. Наша команда имеет более 5 лет опыта в разработке мобильных приложений для доставки, и мы реализовали более 15 систем отслеживания заказов для интернет-магазинов.
Таймлайн статусов и карта курьера — это два разных механизма с разными требованиями к обновлению, и смешивать их в одном polling-запросе — первая архитектурная ошибка. Мы уже опробовали несколько подходов и выделили оптимальные решения для каждого канала.
Почему статусы и координаты требуют разных каналов?
Статус заказа меняется редко — 5-7 раз за весь жизненный цикл. Для этого подходит long-polling или SSE (Server-Sent Events): соединение открыто, сервер пушит событие только при смене статуса. WebSocket здесь избыточен, хотя его часто выбирают по инерции. Координаты курьера обновляются каждые 15-30 секунд — это уже другой профиль нагрузки.
| Критерий | Polling | SSE | WebSocket |
|---|---|---|---|
| Частота | Любая | Редкие события | Частые события |
| Задержка | Зависит от интервала | Низкая | Низкая |
| Сложность | Простая | Средняя | Высокая |
| Пример | Статусы заказа | Таймлайн | Координаты курьера |
Средняя задержка обновления статуса через SSE составляет менее 1 секунды, а при использовании WebSocket для координат — до 200 мс.
Как правильно реализовать таймлайн на клиенте?
На iOS реализация через URLSession с SSE выглядит так:
let request = URLRequest(url: URL(string: "https://api.example.com/orders/\(orderId)/status-stream")!) let task = URLSession.shared.dataTask(with: request) { data, response, error in // парсим text/event-stream построчно } task.resume() Лучше использовать готовую библиотеку — IVALiveEventSource или Swift Package swift-eventsource от LaunchDarkly. Они корректно обрабатывают reconnect и heartbeat.
На Android — OkHttp с EventSource из библиотеки com.launchdarkly:okhttp-eventsource. Нативный HttpURLConnection для SSE писать вручную — трата времени на обработку краевых случаев.
Сравнение библиотек для SSE:
| Платформа | Библиотека | Преимущества |
|---|---|---|
| iOS | IVALiveEventSource | Поддержка heartbeat, автоматический reconnect |
| iOS | swift-eventsource (LaunchDarkly) | Активное сообщество, совместимость с Swift Package Manager |
| Android | okhttp-eventsource (LaunchDarkly) | Интеграция с OkHttp, простота настройки |
Структура таймлайна на экране
Для отображения прогресса статусов используют RecyclerView (Android) или UICollectionView с кастомным layout (iOS). Типичная ошибка — хранить «прошедшие» статусы только на клиенте. Если пользователь удалил и переустановил приложение, история пропадёт. Все пройденные статусы с временными метками должны возвращаться с сервера в массиве:
{ "currentStatus": "courier_assigned", "timeline": [ { "status": "created", "timestamp": "2025-01-01T10:00:00Z" }, { "status": "confirmed", "timestamp": "2025-01-01T10:02:30Z" }, { "status": "courier_assigned", "timestamp": "2025-01-01T10:15:00Z" } ] } Как обрабатывать координаты курьера отдельно?
Координаты курьера обновляются каждые 15-30 секунд — это уже не SSE, а WebSocket или отдельный polling с коротким интервалом. Смешивать его с таймлайном статусов в одном endpoint-е значит либо перегружать статусный поток, либо обновлять карту слишком редко.
На практике делаем так:
- Статусы — SSE или push-уведомления (Firebase Cloud Messaging)
- Координаты курьера — WebSocket с интервалом 15-30 секунд или polling
/orders/{id}/courier-location
Сглаживание маркера курьера
Маркер курьера на карте прыгает, если просто устанавливать новые координаты напрямую. Правильно — анимировать перемещение между точками. На Android через ValueAnimator:
val animator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 1000 addUpdateListener { animation -> val fraction = animation.animatedValue as Float val lat = startLat + (endLat - startLat) * fraction val lng = startLng + (endLng - startLng) * fraction courierMarker.position = LatLng(lat, lng) } } animator.start() На iOS через CADisplayLink или UIView.animate с промежуточными координатами.
Вращение иконки курьера по направлению движения считается через atan2(deltaLat, deltaLng) — не забудьте перевести радианы в градусы для marker.rotation.
Что делать при обрыве соединения?
Пользователь сворачивает приложение — WebSocket и SSE разрываются. Ключевые смены статуса (курьер забрал заказ, курьер рядом, заказ доставлен) дублируются через FCM/APNs. На iOS нужен UNUserNotificationCenter, на Android — FirebaseMessagingService.
Тонкость: уведомление «курьер рядом» теряет смысл, если пришло через 10 минут после доставки. Сервер должен проверять актуальность события перед отправкой пуша — это серверная логика, не мобильная.
Что входит в работу
- Таймлайн статусов с SSE или FCM-пушами
- Карта курьера с анимированным маркером (Google Maps SDK или MapKit)
- WebSocket или polling для координат с правильным lifecycle (onPause/onResume / viewDidDisappear)
- Обработка оффлайн-состояния: очередь событий и синхронизация при восстановлении сети
- Code signing для push-уведомлений (APNs и FCM)
Сроки
3–5 дней для полного flow: таймлайн + карта + пуши. Только таймлайн без карты — 1–2 дня. Стоимость рассчитывается индивидуально после анализа требований.
Получите консультацию по вашему проекту. Мы поможем выбрать оптимальную архитектуру для вашего приложения доставки. Закажите оценку — это бесплатно.







