ארכיטקטורת מעקב הזמנות: שני ערוצי נתונים
הלקוח פותח את האפליקציה 40 דקות לאחר ביצוע ההזמנה ורואה את הסטטוס "בטיפול"—זהה לזה שמיד לאחר התשלום. הם מתקשרים לתמיכה. הבעיה אינה לוגיסטית: השליח כבר בדרך, הקואורדינטות מתעדכנות בשרת כל 30 שניות. הבעיה היא שהאפליקציה הניידת אינה מחוברת לזרם הזה. נתקלנו בכך ב-80% מהפרויקטים בתחילת הדרך. לצוות שלנו יש ניסיון של למעלה מ-5 שנים בבניית אפליקציות משלוחים ניידות, ויישמנו יותר מ-15 מערכות מעקב הזמנות לחנויות מסחר אלקטרוני.
ציר הזמן של הסטטוסים ומפת השליח הם שני מנגנונים שונים עם דרישות עדכון שונות, וערבובם בבקשת polling אחת היא הטעות הארכיטקטונית הראשונה. כבר בדקנו מספר גישות וזיהינו פתרונות אופטימליים לכל ערוץ.
מדוע סטטוסים וקואורדינטות דורשים ערוצים שונים
שינויי סטטוס הזמנה מתרחשים לעיתים רחוקות—5-7 פעמים במהלך כל מחזור החיים. לשם כך, long-polling או SSE (Server-Sent Events) עובדים: החיבור נשאר פתוח, השרת דוחף אירוע רק כאשר הסטטוס משתנה. WebSocket הוא מיותר כאן, אם כי לעיתים קרובות נבחר מתוך הרגל. קואורדינטות השליח מתעדכנות כל 15-30 שניות—זהו פרופיל עומס שונה.
| קריטריון | Polling | SSE | WebSocket |
|---|---|---|---|
| תדירות | כל שהיא | אירועים נדירים | אירועים תכופים |
| השהיה | תלויה במרווח | נמוכה | נמוכה |
| מורכבות | פשוטה | בינונית | גבוהה |
| דוגמה | סטטוס הזמנה | ציר זמן | קואורדינטות שליח |
ההשהיה הממוצעת לעדכוני סטטוס באמצעות SSE היא פחות משנייה אחת, ולקואורדינטות באמצעות WebSocket עד 200 אלפיות השנייה.
כיצד ליישם כראוי את ציר הזמן בצד הלקוח
ב-iOS, יישום SSE עם URLSession נראה כך:
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()עדיף להשתמש בספרייה מוכנה—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() או חבילת Swift IVALiveEventSource מ-LaunchDarkly. הם מטפלים כראוי בחיבור מחדש ו-heartbeat.
ב-Android—swift-eventsource עם OkHttp מהספרייה EventSource. כתיבת com.launchdarkly:okhttp-eventsource מקורי עבור SSE ידנית היא בזבוז זמן בטיפול במקרי קצה.
השוואת ספריות SSE:
| פלטפורמה | ספרייה | יתרונות |
|---|---|---|
| iOS | IVALiveEventSource | תמיכה ב-heartbeat, חיבור מחדש אוטומטי |
| iOS | swift-eventsource (LaunchDarkly) | קהילה פעילה, תאימות ל-Swift Package Manager |
| Android | okhttp-eventsource (LaunchDarkly) | אינטגרציה עם OkHttp, קלות הגדרה |
מבנה ציר הזמן על המסך
להצגת התקדמות הסטטוסים, השתמש ב-HttpURLConnection (Android) או ב-RecyclerView עם 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
UICollectionView
החלקת סמן השליח
סמן השליח על המפה קופץ אם פשוט מגדירים קואורדינטות חדשות ישירות. הדרך הנכונה היא להנפיש את התנועה בין נקודות. ב-Android באמצעות { "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" } ] } :
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 באמצעות /orders/{id}/courier-location או 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() —זכור להמיר רדיאנים למעלות עבור CADisplayLink.
מה לעשות כאשר החיבור נופל?
המשתמש ממזער את האפליקציה—WebSocket ו-SSE מתנתקים. שינויי סטטוס מרכזיים (השליח אסף, השליח בקרבת מקום, נמסר) משוכפלים באמצעות FCM/APNs. ב-iOS השתמש ב-UIView.animate, ב-Android ב-atan2(deltaLat, deltaLng).
ניואנס: הודעת "השליח בקרבת מקום" מאבדת משמעות אם היא מגיעה 10 דקות לאחר המסירה. השרת חייב לבדוק את עדכניות האירוע לפני שליחת push—זוהי לוגיקה בצד השרת, לא בנייד.
מה כלול בעבודה
- ציר זמן סטטוסים עם SSE או הודעות FCM
- מפת שליח עם סמן מונפש (Google Maps SDK או MapKit)
- WebSocket או polling לקואורדינטות עם מחזור חיים נכון (onPause/onResume / viewDidDisappear)
- טיפול במצב לא מקוון: תור אירועים וסנכרון בעת שחזור הרשת
- חתימת קוד להודעות push (APNs ו-FCM)
לוחות זמנים
3–5 ימים לזרימה המלאה: ציר זמן + מפה + הודעות push. ציר זמן בלבד ללא מפה—1–2 ימים. העלות מחושבת באופן אישי לאחר ניתוח דרישות.
קבל ייעוץ לפרויקט שלך. נעזור לך לבחור את הארכיטקטורה האופטימלית לאפליקציית המשלוחים שלך. בקש הערכת עלות.







