Разработка мобильного приложения для оплаты парковки
Приложение для оплаты парковки — это не просто кнопка «Оплатить». Ключевая сложность здесь — сессионная модель: парковочная сессия начинается при въезде, заканчивается при выезде или по истечению купленного времени. Пользователь должен знать, сколько времени осталось, и получить предупреждение до того, как начнутся штрафные санкции. Всё это требует точной работы push-уведомлений, фоновых таймеров и надёжной интеграции с платёжным шлюзом.
Как устроена парковочная сессия
Центральная сущность — ParkingSession. У неё есть жизненный цикл:
IDLE → ACTIVE → EXPIRING (за 15 мин до конца) → EXPIRED / EXTENDED
Переходы состояний управляются на сервере. Приложение отображает актуальное состояние через polling или WebSocket. Фоновый таймер на устройстве — только для UI, не для бизнес-логики.
Типичный объект сессии:
{
"sessionId": "PSN-20240921-4471",
"zoneCode": "A-12",
"vehiclePlate": "А123ВС77",
"startedAt": "2024-09-21T10:15:00+03:00",
"expiresAt": "2024-09-21T12:15:00+03:00",
"rate": 60,
"currency": "RUB",
"status": "ACTIVE",
"paymentStatus": "PAID"
}
Распознавание номерного знака через камеру
Ввод номера вручную — плохой UX. Удобнее — распознавание через камеру. На iOS используем Vision + VNRecognizeTextRequest:
import Vision
func recognizePlate(from pixelBuffer: CVPixelBuffer) {
let request = VNRecognizeTextRequest { [weak self] request, error in
guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
let candidates = observations.compactMap { $0.topCandidates(1).first?.string }
let plateRegex = /[АВЕКМНОРСТУХ]{1}\d{3}[АВЕКМНОРСТУХ]{2}\d{2,3}/
let plate = candidates.compactMap { $0.firstMatch(of: plateRegex)?.0 }.first
DispatchQueue.main.async {
self?.vehiclePlateField.text = plate.map(String.init) ?? ""
}
}
request.recognitionLevel = .accurate
request.recognitionLanguages = ["ru-RU"]
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)
try? handler.perform([request])
}
На Android аналогично через ML Kit Text Recognition:
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
val image = InputImage.fromMediaImage(mediaImage, rotationDegrees)
recognizer.process(image)
.addOnSuccessListener { visionText ->
val plateRegex = Regex("[АВЕКМНОРСТУХ]{1}\\d{3}[АВЕКМНОРСТУХ]{2}\\d{2,3}")
val plate = visionText.textBlocks
.flatMap { it.lines }
.mapNotNull { plateRegex.find(it.text)?.value }
.firstOrNull()
vehiclePlateField.setText(plate)
}
Точность распознавания российских номеров на качественных снимках — около 85–90%. Для повышения точности дополнительно применяем OpenALPR через серверный API для сложных случаев.
Таймер сессии и push-уведомления
Таймер обратного отсчёта — самый часто запрашиваемый UI-элемент. На iOS он живёт в ProgressView + Timer, на Android в CountDownTimer. Но фоновые уведомления — через APNs/FCM.
Логика отправки уведомлений на сервере:
- За 15 минут до
expiresAt→ push «Парковка истекает через 15 минут» - За 5 минут → push с кнопками «Продлить на 1 час» / «Завершить»
- В момент
expiresAt→ push «Парковочная сессия завершена»
На iOS кнопки в push-уведомлении реализуются через UNNotificationCategory:
let extendAction = UNNotificationAction(
identifier: "EXTEND_PARKING",
title: "Продлить на 1 час",
options: [.foreground]
)
let stopAction = UNNotificationAction(
identifier: "STOP_PARKING",
title: "Завершить",
options: [.destructive]
)
let category = UNNotificationCategory(
identifier: "PARKING_EXPIRING",
actions: [extendAction, stopAction],
intentIdentifiers: []
)
UNUserNotificationCenter.current().setNotificationCategories([category])
Нажатие «Продлить» из уведомления — приложение открывается на экране продления и автоматически инициирует платёж сохранённой картой без лишних шагов.
Интеграция с парковочным оборудованием
Если парковка управляется СКУД или шлагбаумом, интеграция ведётся через серверный API оператора. Распространённые протоколы: SOAP/XML (legacy-системы), REST JSON (современные). Приложение не общается с оборудованием напрямую — только через бэкенд.
Для открытия шлагбаума через QR-код на выезде используем AVCaptureSession:
let session = AVCaptureSession()
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device) else { return }
session.addInput(input)
let output = AVCaptureMetadataOutput()
session.addOutput(output)
output.setMetadataObjectsDelegate(self, queue: .main)
output.metadataObjectTypes = [.qr]
session.startRunning()
Платёжный flow
Оплата парковки обычно реализуется двумя сценариями:
Pre-paid — покупаем время до въезда. Пользователь выбирает зону, время, платит. Сервер выдаёт код сессии. На въезде оператор сканирует QR или считывает номер.
Post-paid — оплата при выезде. Сессия начинается автоматически при въезде (по номеру), сумма рассчитывается при выезде, приложение предлагает оплатить.
Для обоих вариантов используем сохранённую карту через токен провайдера (CloudPayments, ЮKassa, Stripe). Разовый платёж без сохранения карты — через платёжный веб-виджет в WKWebView/WebView. Регулярные платежи (подписка на парковочный абонемент) — через recurring payments с токеном.
Стек и архитектура
| Компонент | iOS | Android |
|---|---|---|
| UI | SwiftUI + UIKit (камера) | Jetpack Compose + CameraX |
| Распознавание номеров | Vision Framework | ML Kit Text Recognition |
| Карты | MapKit / Google Maps SDK | Google Maps SDK |
| Платежи | Stripe iOS SDK / CloudPayments | Stripe Android SDK / CloudPayments |
| Push | APNs через Firebase | FCM |
| Архитектура | MVVM + Combine | MVVM + StateFlow |
Ориентиры по срокам
Базовая версия (сессии, таймер, оплата картой, push): 4–6 недель. Добавление распознавания номеров, интеграции с оборудованием, абонементов — ещё 2–4 недели. Стоимость рассчитывается индивидуально после анализа требований.







