Мы разрабатываем аукционные приложения, где каждая миллисекунда задержки — потерянный лот. Представьте: на торгах за редкий предмет остаётся 30 секунд, и вдруг сервер не отвечает 2 секунды — пользователь лишается шанса сделать ставку. Такие сценарии мы закрываем архитектурой на WebSocket и оптимистической блокировкой. Ниже разберём ключевые технические решения: WebSocket с автопереподключением, обработку гонки условий, автоставки (proxy bidding) и anti-sniping.
Аукционное приложение — это не просто каталог товаров. Его сердце — реальное время, где согласованность данных критична. Без правильной системы версий и бизнес-логики пользователи будут видеть устаревшие цены и терять ставки. Рассмотрим проверенную архитектуру, выдерживающую тысячи одновременных соединений.
Как разработать мобильное приложение для аукциона с нуля?
Проектирование начинается с выбора транспорта и протокола синхронизации. Мы используем WebSocket (протокол описан в RFC 6455) и оптимистическую блокировку на уровне базы данных. Определяем события: BID_PLACED, AUCTION_EXTENDED, AUCTION_ENDED, YOU_WON, YOU_WERE_OUTBID. Каждое событие содержит версию лота для проверки согласованности.
Почему WebSocket, а не polling?
Polling для аукциона — неприемлемо: задержка в 5-10 секунд при опросе HTTP приводит к потерянным ставкам. WebSocket с переподключением — стандарт, обеспечивающий задержку <100 мс. Это в 50-100 раз быстрее, чем polling, что критично для аукционов с высокой активностью.
| Метод | Задержка | Надёжность |
|---|---|---|
| Polling (HTTP) | 5-10 секунд | Средняя — частые запросы нагружают сервер |
| WebSocket | <100 мс | Высокая — автоматическое восстановление соединения |
Реализация на Swift и Kotlin:
Swift WebSocket (URLSessionWebSocketTask)
// iOS: WebSocket через URLSessionWebSocketTask class AuctionWebSocket { private var webSocketTask: URLSessionWebSocketTask? private var reconnectTimer: Timer? func connect(auctionId: String) { let url = URL(string: "wss://api.yourauction.com/auctions/\(auctionId)/live")! webSocketTask = URLSession.shared.webSocketTask(with: url) webSocketTask?.resume() receive() } private func receive() { webSocketTask?.receive { [weak self] result in switch result { case .success(let message): if case .string(let text) = message { self?.handleMessage(text) } self?.receive() // продолжаем слушать case .failure: self?.scheduleReconnect() } } } private func scheduleReconnect() { reconnectTimer?.invalidate() reconnectTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in self?.connect(auctionId: self?.currentAuctionId ?? "") } } } Kotlin WebSocket (OkHttp)
// Android: OkHttp WebSocket class AuctionWebSocketManager( private val client: OkHttpClient, private val scope: CoroutineScope ) { private val _events = MutableSharedFlow<AuctionEvent>() val events: SharedFlow<AuctionEvent> = _events.asSharedFlow() fun connect(auctionId: String) { val request = Request.Builder() .url("wss://api.yourauction.com/auctions/$auctionId/live") .build() client.newWebSocket(request, object : WebSocketListener() { override fun onMessage(webSocket: WebSocket, text: String) { scope.launch { val event = json.decodeFromString<AuctionEvent>(text) _events.emit(event) } } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { scope.launch { delay(3000) connect(auctionId) } } }) } } Как избежать гонки условий при одновременных ставках?
Самая сложная бизнес-логика: два пользователя делают ставку одновременно. Пример:
- Текущая ставка: 1000 ₽
- Пользователь A видит 1000 ₽, делает ставку 1100 ₽
- Пользователь B видит 1000 ₽, делает ставку 1050 ₽
- Обе ставки приходят на сервер в один момент
Правильное решение — оптимистическая блокировка с версией лота. Она лучше пессимистической блокировки, так как не блокирует строку на чтение и обеспечивает высокую пропускную способность. Сервер проверяет версию лота перед обновлением:
def place_bid(user_id: str, lot_id: str, amount: Decimal, expected_version: int) -> BidResult: with db.transaction(): lot = db.select_for_update(f"SELECT * FROM lots WHERE id = %s", lot_id) if lot.version != expected_version: return BidResult( success=False, reason="LOT_UPDATED", current_bid=lot.current_bid, new_version=lot.version ) if amount <= lot.current_bid: return BidResult( success=False, reason="BID_TOO_LOW", current_bid=lot.current_bid, new_version=lot.version ) db.execute( "UPDATE lots SET current_bid=%s, current_bidder=%s, version=version+1 WHERE id=%s", (amount, user_id, lot_id) ) db.execute( "INSERT INTO bids (lot_id, user_id, amount) VALUES (%s, %s, %s)", (lot_id, user_id, amount) ) broadcast_to_websockets(lot_id, { "type": "BID_PLACED", "amount": str(amount), "bidder": mask_username(user_id), "version": lot.version + 1 }) return BidResult(success=True, new_version=lot.version + 1) При LOT_UPDATED клиент получает актуальную ставку и может предложить пользователю сделать новую с учётом текущей цены. Так мы избегаем двойных списаний и некорректного баланса.
Как работает автоставка?
Пользователь устанавливает максимальную сумму, которую готов заплатить. Система автоматически повышает его ставку в ответ на ставки конкурентов — до установленного максимума.
def process_autobid(lot_id: str, new_bid_amount: Decimal, new_bidder_id: str): """Проверяем автоставки после каждой новой ставки""" autobids = db.get_active_autobids(lot_id, exclude_user=new_bidder_id) for autobid in sorted(autobids, key=lambda x: x.max_amount, reverse=True): counter_amount = new_bid_amount + lot.bid_step if counter_amount <= autobid.max_amount: # Автоматически ставим от имени держателя автоставки place_bid(autobid.user_id, lot_id, counter_amount, lot.version) break Автоставка — источник конфликтов с пользователями, поэтому важна детальная история: «Ваша ставка 1 200 ₽ была автоматически повышена в ответ на ставку 1 100 ₽». Пользователь всегда видит, что произошло, и может скорректировать лимит.
Что такое anti-sniping?
Снайпинг — ставка в последние секунды. Чтобы его избежать, аукцион продлевается при ставке в конце:
ANTI_SNIPING_THRESHOLD = timedelta(minutes=2) ANTI_SNIPING_EXTENSION = timedelta(minutes=2) def after_bid_placed(lot_id: str): lot = db.get_lot(lot_id) time_remaining = lot.ends_at - datetime.utcnow() if time_remaining < ANTI_SNIPING_THRESHOLD: new_end_time = lot.ends_at + ANTI_SNIPING_EXTENSION db.update_lot_end_time(lot_id, new_end_time) broadcast_to_websockets(lot_id, { "type": "AUCTION_EXTENDED", "new_end_time": new_end_time.isoformat() }) Таймер на клиенте синхронизируется с серверным временем при получении AUCTION_EXTENDED. Это даёт всем участникам равные шансы на ответную ставку.
Как организовать платежи?
Победитель получает push-уведомление и имеет ограниченное время (24–48 часов) для оплаты. Если не оплатил — лот переходит следующему по ставке или выставляется повторно.
Для ценных лотов — предоплата перед участием: депозит блокируется при регистрации на торги, возвращается проигравшим.
Комиссия с продажи (покупателя или продавца) удерживается при оплате. Payouts продавцу — через Stripe Connect или аналог. Если вам нужна нестандартная схема расчётов, свяжитесь с нами — подберём решение.
Ориентиры по срокам
| Scope | Срок |
|---|---|
| Просмотр лотов, WebSocket, ставки, push-уведомления | 6–8 недель |
| Автоставки, anti-sniping, история ставок | +2 недели |
| Кабинет продавца, публикация лотов, payouts | +3–4 недели |
| Депозиты и escrow | +1–2 недели |
Стоимость рассчитывается индивидуально после анализа требований.
Что входит в работу?
- Техническое задание и прототип UI/UX
- Разработка под iOS (Swift, SwiftUI) и Android (Kotlin, Jetpack Compose)
- Backend на Python/FastAPI или Node.js
- Интеграция WebSocket с переподключением
- Реализация автоставок и anti-sniping
- Платёжная система (депозиты, escrow, payouts)
- Тестирование (unit, UI, load)
- Публикация в App Store и Google Play
- Документация и обучение администраторов
- Гарантийная поддержка 3 месяца
Получите консультацию по вашему проекту. Мы оценим сроки и предложим оптимальное решение. Наш опыт гарантирует стабильную работу аукциона даже при высокой нагрузке. Свяжитесь с нами, чтобы обсудить архитектуру и детали реализации.







