פיתוח אפליקציית מובייל למכירת כרטיסים לאירועים
הכאב הטכני העיקרי של אפליקציית מכירת כרטיסים הוא מכירה תחרותית. כאשר 500 אנשים לוחצים בו-זמנית על "קנייה" עבור 10 המושבים האחרונים, האפליקציה חייבת לטפל בכך כראוי ללא הזמנות כפולות, הקפאות או סטטוסים שגויים. זו לא משימת פרונטאנד — אנו מתכננים את הצד השרת עם נעילה פסימית והזמנה מבוססת תור. לקוח המובייל מוכן לתרחיש "המושב כבר תפוס" עם UX ברור ומשוב מיידי.
אנו יוצרים אפליקציות מכירת כרטיסים מוכנות: ממפות מושבים אינטראקטיביות ועד אפליקציית בקר. הניסיון שלנו בתחום זה הוא 5+ שנים עם למעלה מ-20 פרויקטים מוצלחים. אנו מבטיחים עמידות בתנאי עומס ועמידה בהנחיות סקירת App Store (סעיף 4.2/5.1).
הזמנת מושבים ותנאי מרוץ
התכנית הסטנדרטית: משתמש בוחר מושב → השרת יוצר reservation עם TTL של 10 דקות → המשתמש משלם → ה-reservation מומר ל-ticket. אם התשלום לא הושלם תוך 10 דקות, המושב משוחרר.
בקוח המובייל, טיימר ההזמנה הוא CountDownTimer (אנדרואיד) או Timer.scheduledTimer (iOS) המסונכרן עם TTL של השרת. לא מרגע לחיצת הכפתור בקוח, אלא מ-reservation.expiresAt מתגובת השרת. הבדלי אזורי זמן וסחיפה של שעון המכשיר יהרגו את טיימר ה-UI אם לא מסונכרן כראוי.
// Android: синхронизация таймера с серверным expiresAt
class ReservationViewModel : ViewModel() {
private val _timeLeft = MutableStateFlow(0L)
val timeLeft: StateFlow<Long> = _timeLeft
fun startCountdown(expiresAt: Instant) {
viewModelScope.launch {
while (true) {
val remaining = ChronoUnit.SECONDS.between(Instant.now(), expiresAt)
if (remaining <= 0) {
_timeLeft.emit(0)
onReservationExpired()
break
}
_timeLeft.emit(remaining)
delay(1000)
}
}
}
}כאשר ההזמנה פגה, אנחנו לא רק מציגים שגיאה. אנו מציעים אוטומטית את המושבים הפנויים הקרובים ביותר אם זרם בזמן אמת (WebSocket או SSE) תומך בכך.
איך להימנע מהזמנות כפולות?
הזמנה כפולה היא בעיה קלאסית עם בקשות מקבילות. אנו פותרים אותה באמצעות // Android: синхронизация таймера с серверным expiresAt class ReservationViewModel : ViewModel() { private val _timeLeft = MutableStateFlow(0L) val timeLeft: StateFlow<Long> = _timeLeft fun startCountdown(expiresAt: Instant) { viewModelScope.launch { while (true) { val remaining = ChronoUnit.SECONDS.between(Instant.now(), expiresAt) if (remaining <= 0) { _timeLeft.emit(0) onReservationExpired() break } _timeLeft.emit(remaining) delay(1000) } } } } ב-PostgreSQL או נעילה מבוזרת ברמת SELECT FOR UPDATE ב-Redis. חלופה היא reservationId + seatId בעת הכנסת ההזמנה. בקוח, אנו משכפלים את הנעילה: כפתור "קנייה" מושבת לאחר הלחיצה הראשונה, ובקשות חוזרות נשלחות עם מפתח אידמפוטנטיות ייחודי. שיטות אלה מפחיתות את הסבירות לקונפליקט פי 3 בעומסי שיא.
מפת מושבים ומקום אינטראקטיבי
לאולמות קונצרטים ותיאטראות, יש צורך במפת מושבים אינטראקטיבית. ב-iOS — ON CONFLICT DO NOTHING עם UICollectionView מותאם אישית או UICollectionViewLayout ב-SwiftUI למקומות עם גיאומטריה לא סטנדרטית. באנדרואיד — Canvas + Canvas לצביטה לזום.
המפה נטענת כ-JSON עם קואורדינטות של כל מושב, אזור וסטטוס (GestureDetector, available, reserved, sold). עדכוני סטטוס בזמן אמת — דרך WebSocket: ברגע שמישהו מזמין מושב, כל הלקוחות המחוברים מקבלים disabled.
| פלטפורמה | טכנולוגיה | יתרון |
|---|---|---|
| iOS | SwiftUI Canvas + UIKit | ביצועים מקוריים, תמיכה ב-bezier |
| אנדרואיד | Canvas + GestureDetector | צביטה חלקה לזום, גיאומטריה מותאמת אישית |
| חוצה פלטפורמות | react-native-svg / flutter_svg | קוד יחיד, רינדור SVG מהיר |
| טכנולוגיית עדכון | זמן השהיה | עומס שרת |
|---|---|---|
| WebSocket | ~100 אלפיות שנייה | נמוך (חיבור קבוע) |
| פולינג (כל 5 שניות) | ~5 שניות | גבוה (בקשות תכופות) |
מפות מקום SVG הן בחירה פופולרית לפתרונות חוצי פלטפורמות. React Native עם { type: "seat_reserved", seatId: "A-15" } או Flutter עם react-native-svg + flutter_svg מרנדרים SVG עם יכולת לחיצה על אלמנטים.
למה עדכון בזמן אמת חשוב?
ללא WebSocket, משתמש עלול לבחור מושב שכבר תפוס. זה מוביל לתסכול ואובדן מכירות. אנו מיישמים חיבורי WebSocket עם חיבור מחדש אוטומטי (exponential backoff). ברגע שהזמנה משתנה, הלקוח מקבל אירוע ומצייר מחדש את המפה. למצב לא מקוון, אנו שומרים במטמון את המצב האחרון ומציגים אזהרה.
כרטיסים אלקטרוניים ואימות כניסה
כל כרטיס שנרכש מקבל UUID ייחודי וקוד QR שנוצר בשרת. אין צורך לאחסן סוד ב-QR — רק GestureDetector; האימות נעשה בשרת בעת הסריקה. ה-QR לא צריך להיות תמונה סטטית ב-PDF — אנו מייצרים אותו דינמית מ-ticketId בקוח באמצעות ZXingObjC (iOS) או zxing-android-embedded, כדי להימנע מאחסון תמונות רסטר במסד הנתונים.
אפליקציית הבקר — מסך נפרד או אפליקציה נפרדת עם סורק באמצעות ticketId/AVFoundation, POST ל-CameraX, תגובה: /tickets/{id}/validate / valid / already_used. אנו שומרים במטמון invalid מאומתים מקומית במכשיר הבקר במקרה של חוסר אינטרנט — סנכרון לאחר שחזור הקישוריות.
תהליך התשלום
רכישה — YooKassa, CloudPayments או Stripe. ב-iOS, בנוסף Apple Pay (ticketId), באנדרואיד, Google Pay. לפלח B2B — חשבונית בדוא"ל עם תשלום דרך SBP (מערכת תשלומים מהירה).
לאחר התשלום, שליחה מיידית של כרטיס PDF בדוא"ל דרך SendGrid/Postmark והודעת push דרך FCM/APNs עם קישור עמוק למסך "הכרטיסים שלי".
איך אנחנו עושים את זה: מחסנית ותהליך
אנו מתחילים בניתוח דרישות העסקיות והאילוצים הטכניים שלך. המחסנית האופיינית שלנו כוללת PostgreSQL, Redis, Java/Kotlin בקצה השרת, Swift/Kotlin לאפליקציות מובייל מקוריות, ו-WebSocket לעדכונים בזמן אמת. אנו עוקבים אחר ארכיטקטורה מודולרית כדי להבטיח מדרגיות ותחזוקה.
דוגמה לתצורת מטמון בקר
CREATE TABLE ticket_validations ( ticket_id UUID PRIMARY KEY, validated_at TIMESTAMP DEFAULT NOW() ); כאשר הקישוריות משוחזרת, אנו שולחים את כל PKPaymentRequest לשרת לסנכרון.
שגיאות אופייניות
- הזמנה כפולה. ללא
CREATE TABLE ticket_validations ( ticket_id UUID PRIMARY KEY, validated_at TIMESTAMP DEFAULT NOW() );או נעילה מבוזרת, שני משתמשים יכולים ליצור הזמנה לאותו מושב. - QR על צילום מסך. אין ליישם הגנת צילום מסך. ה-QR צריך להיות חד-פעמי בעת האימות.
- סנכרון זמן. השתמש תמיד ב-
ticket_idבצד השרת, לא בזמן מקומי.
שלבים ולוח זמנים
עיצוב מודל נתונים → מפת מושבים → הזמנה עם TTL → תשלום → כרטיסים אלקטרוניים → אפליקציית בקר → בדיקת עומס (1000+ משתמשים בו-זמנית).
6–10 שבועות לאפליקציה מלאה עם מפת מושבים אינטראקטיבית ועדכונים בזמן אמת. העלות מחושבת בנפרד לאחר ניתוח דרישות.
קבל ייעוץ על ארכיטקטורת מכירת כרטיסים. הזמן פיתוח לפרויקט שלך — נכין הערכה תוך יום אחד.







