Разработка мобильного приложения для электронного билета (Digital Ticket)
Электронный билет — это не просто PDF с QR-кодом в виде файла. Современный Digital Ticket — это живой объект: он обновляется при переносе мероприятия, показывает схему зала, позволяет зайти через NFC или Wallet, и валидируется сканером без интернета на стороне контролёра.
Форматы хранения и передачи билета
Приложение должно поддерживать несколько каналов хранения:
In-app storage — билет хранится в приложении как объект в базе (Core Data / Room), QR генерируется на лету из ticketToken. Требует интернета для первой загрузки.
Apple Wallet / Google Wallet — добавляется через PKAddPassesViewController или Google Pay SDK. Доступен без интернета, обновляется через push. Покрывается отдельной интеграцией.
PDF — генерируется на сервере (PDFKit/wkhtmltopdf), скачивается пользователем. Используется как резервный вариант.
Для большинства приложений делаем все три — пользователь сам выбирает удобный вариант.
Генерация и валидация QR
QR-код должен содержать не просто номер заказа, а подписанный токен — иначе его можно подделать, скопировав изображение экрана.
Схема: сервер генерирует ticketToken = HMAC-SHA256(ticketId + userId + expiresAt, secret). Контролёр сканирует QR → приложение контролёра отправляет токен на POST /tickets/validate → сервер проверяет HMAC и статус использования.
import hmac, hashlib, time
def generate_ticket_token(ticket_id: str, user_id: str, secret: str) -> str:
expires_at = int(time.time()) + 86400 * 30 # действителен 30 дней
message = f"{ticket_id}:{user_id}:{expires_at}"
signature = hmac.new(
secret.encode(), message.encode(), hashlib.sha256
).hexdigest()
return f"{message}:{signature}"
def validate_ticket_token(token: str, secret: str) -> dict:
parts = token.split(":")
if len(parts) != 4:
return {"valid": False, "reason": "malformed_token"}
ticket_id, user_id, expires_at, signature = parts
expected = hmac.new(
secret.encode(),
f"{ticket_id}:{user_id}:{expires_at}".encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return {"valid": False, "reason": "invalid_signature"}
if int(expires_at) < int(time.time()):
return {"valid": False, "reason": "expired"}
return {"valid": True, "ticketId": ticket_id, "userId": user_id}
Для событий с высоким риском перепродажи используют rotating QR — токен меняется каждые 30–60 секунд (TOTP-логика). Скриншот немедленно устаревает.
Оффлайн-валидация
Контролёр на входе может быть без интернета. Два подхода:
Оффлайн-список — приложение контролёра загружает список валидных ticketId заранее (например, за час до начала). Сканирует QR, проверяет по локальному списку. Риск: нельзя пометить билет как использованный до синхронизации.
Цифровая подпись без сервера — контролёр верифицирует HMAC с публичным ключом, встроенным в приложение. При этом PAN не хранится, а только публичный ключ. Отозвать отдельный билет оффлайн нельзя — только черный список, загружаемый заранее.
Схема зала и выбор мест
Если мероприятие предполагает нумерованные места — нужна интерактивная схема зала. Реализуется через SVG или кастомный Canvas. На React Native удобен react-native-svg, на Flutter — CustomPainter.
// Flutter: кастомный painter для рядов кресел
class SeatMapPainter extends CustomPainter {
final List<Seat> seats;
final Set<String> selectedSeats;
@override
void paint(Canvas canvas, Size size) {
for (final seat in seats) {
final paint = Paint()
..color = selectedSeats.contains(seat.id)
? Colors.blue
: seat.isAvailable ? Colors.green : Colors.grey;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(seat.x, seat.y, 28, 24),
const Radius.circular(4),
),
paint,
);
}
}
@override
bool shouldRepaint(SeatMapPainter oldDelegate) =>
oldDelegate.selectedSeats != selectedSeats;
}
Схема загружается как JSON с координатами каждого кресла. При масштабировании зала используем InteractiveViewer (Flutter) или UIPinchGestureRecognizer + CATransform3D (iOS).
Возврат и перенос
Логика возврата билета — на сервере. Приложение отображает статус: ACTIVE, USED, REFUNDED, TRANSFERRED. При переносе мероприятия сервер отправляет push → приложение обновляет данные билета.
Важный edge case: билет должен оставаться видимым даже после USED — пользователь хочет видеть историю посещений.
Ориентиры по срокам
Базовая версия (in-app QR, покупка, история): 4–6 недель. Добавление схемы зала с выбором мест — ещё 2–3 недели. Wallet-интеграция (Apple / Google) — ещё 3–5 дней. Стоимость рассчитывается индивидуально.







