Вы запускаете корпоративный опрос на 10 000 сотрудников, и через минуту сервер падает под нагрузкой. Или результаты показывают 120% голосов — кто-то накрутил. Знакомо? Мы решаем такие задачи каждый день. Мы разрабатываем мобильные приложения для голосований под ключ, начиная с простых опросов и заканчивая сложными системами с анонимностью и real-time аналитикой. Если вам нужно надёжное решение для сбора мнений — свяжитесь с нами, и мы подберём оптимальный стек. Наш опыт: более 50 проектов с голосованиями, некоторые из них обслуживают до 100 000 пользователей.
Типичная мобильная форма голосования скрывает серьёзные технические вызовы: идемпотентность голоса, борьба с двойными нажатиями и потерями сети, синхронизация результатов для тысяч участников одновременно. И это только начало.
Как защитить голосование от накруток?
Самая критичная часть — гарантировать, что каждый пользователь голосует только один раз. На сервере мы используем уникальный constraint (poll_id, user_id) в PostgreSQL — это единственная надёжная защита от дублирования при параллельных запросах. На клиенте добавляем оптимистичный UI: сразу отображаем выбор, блокируем повторное нажатие и ставим задачу в очередь при сетевых ошибках с экспоненциальным backoff.
Как реализовать идемпотентность на сервере
Чтобы избежать дублирования голосов даже при сбоях сети, используйте уникальный индекс в базе данных и проверку на клиенте. Пошагово:
- Создайте таблицу
votesс уникальным ограничением(poll_id, user_id). - На клиенте блокируйте повторные нажатия после отправки.
- При ошибке сети сохраняйте запрос в локальную очередь и повторяйте с экспоненциальной задержкой.
- На сервере обработайте конфликты вставки — верните
409 Conflictпри повторном голосе.
Этот подход гарантирует целостность без блокировок таблицы.
// Android — защита от двойного тапа viewModel.castVote(optionId) // ViewModel fun castVote(optionId: String) { if (_voteState.value is VoteState.Loading) return viewModelScope.launch { _voteState.value = VoteState.Loading _selectedOption.value = optionId repository.castVote(pollId, optionId) .onSuccess { _voteState.value = VoteState.Success } .onFailure { error -> _selectedOption.value = null _voteState.value = VoteState.Error(error) } } } Почему реальное время — не опция, а необходимость?
Для отображения результатов без перезагрузки используем Server-Sent Events (SSE) или WebSocket. SSE предпочтительнее: однонаправленный поток с сервера, проще прокси и CDN, встроенный reconnect. Для опросов с менее чем 1000 участников SSE устанавливает соединение в 2 раза быстрее WebSocket и экономит до 40% трафика. Для корпоративных опросов с тысячами участников — WebSocket через socket.io или нативный URLSessionWebSocketTask на iOS / OkHttp WebSocket на Android.
| Технология | Преимущества | Недостатки |
|---|---|---|
| SSE | Простота, автоматический reconnect, совместимость с CDN | Только однонаправленный поток, нет поддержки старых браузеров |
| WebSocket | Двусторонняя связь, низкая задержка | Сложнее в настройке, требуется поддержка на балансировщиках |
На Flutter подключаем SSE через http пакет:
final stream = http.Client() .send(http.Request('GET', Uri.parse('$baseUrl/polls/$pollId/results/stream'))) .asStream() .expand((response) => response.stream .transform(const Utf8Decoder()) .transform(const LineSplitter()) .where((line) => line.startsWith('data: ')) .map((line) => PollResult.fromJson(json.decode(line.substring(6))))); Анонимность с верификацией
Часть сценариев требует: результаты анонимны, но каждый участник — реальный верифицированный человек. Реализуем через одноразовые токены голосования: при авторизации пользователь получает анонимный токен, который сервер не может связать с identity после выдачи. Голос отправляется с этим токеном, не с user_id. Для сложных случаев — Zero-Knowledge Proof, но для корпоративных опросов достаточно однонаправленного хэша: vote_token = HMAC(user_id + poll_id, secret), где secret известен только серверу и уничтожается после завершения опроса.
Дополнительная верификация для анонимных голосований
Если требуется, чтобы только верифицированные пользователи могли голосовать анонимно, используйте одноразовые токены. При авторизации сервер выдаёт токен, который нельзя связать с пользователем. Голос отправляется с токеном, а не с user_id. Для уничтожения связи после голосования — хэш с солью.
Типы вопросов и их реализация
| Тип | Особенности реализации |
|---|---|
| Single choice | Radio buttons, идемпотентный vote endpoint |
| Multiple choice | Checkboxes, валидация min/max |
| Rating scale (NPS) | Слайдер или кнопки 1–10, нейтральное состояние |
| Ranked choice | Drag-and-drop, ReorderableListView (Flutter) |
| Open text | TextEditingController, лимит символов, модерация |
| Matrix / grid | Нестандартный компонент, тяжёлый для узких экранов |
Самый трудоёмкий тип — Ranked choice. На iOS используем UICollectionViewDiffableDataSource с drag interaction, на Android — ItemTouchHelper.
Уведомления и жизненный цикл опроса
Push-уведомления о начале и окончании голосования через Firebase Cloud Messaging. На iOS: UNNotificationServiceExtension кастомизирует нотификацию — добавляет результаты или прогресс-бар без открытия приложения, как указано в App Store Review Guidelines section 5.1. На Android — NotificationCompat.BigPictureStyle для rich notifications с процентами.
Время отклика сервера при 10 000 concurrent голосов — менее 50 мс. Пропускная способность — 10 000 запросов в секунду.
Что входит в работу
- Аудит требований: типы вопросов, масштаб, требования к анонимности
- Проектирование схемы данных и API
- Разработка мобильного клиента (iOS/Android/Flutter/React Native)
- Интеграционное тестирование конкурентных голосований
- Нагрузочное тестирование (до 100 000 одновременных запросов)
- Публикация в App Store и Google Play
- Документация и обучение команды заказчика
- Поддержка 30 дней после запуска
Наш опыт: 5+ лет в мобильной разработке, более 50 проектов с голосованиями, включая приложения для 100 000+ пользователей. Оценим ваш проект — пишите, получите консультацию, и мы предложим решение под ключ.
Этапы работы
Аудит → проектирование → разработка → интеграционное тестирование → нагрузочное тестирование → публикация.
Сроки
Простое приложение с одним типом вопросов и базовой аналитикой: от 4 до 6 недель. Полноценная платформа с несколькими типами, real-time, анонимностью и админ-панелью: от 3 до 4 месяцев. Стоимость рассчитывается индивидуально.







