Без системы отзывов приложение теряет один из главных инструментов социального доказательства — пользователи не видят чужого опыта и не оставляют своего. Мы сталкивались с проектами, где агрегированный рейтинг перекошен из-за нескольких ранних отзывов, фотографии грузятся 4 секунды, а модерацию обходят боты. Разрабатываем систему отзывов под ключ за срок от 3 рабочих дней — звёздный рейтинг, текстовые отзывы, модерация, фото и ответы бизнеса. Получите консультацию — оценим ваш проект бесплатно.
Что обычно ломается в самописных реализациях
Агрегация и обновление рейтинга
Самая частая ошибка — считать средний рейтинг на лету SELECT AVG(rating) по всей таблице отзывов при каждом запросе страницы продукта. При 50 000 отзывов это начинает тормозить. Правильный подход: денормализованное поле average_rating и reviews_count на стороне сервера, обновляемое через триггер или очередь (Celery/Sidekiq/BullMQ) при добавлении/изменении/удалении отзыва. Клиент получает уже готовое значение.
На мобиле рейтинг нужно отображать как звёздный индикатор — iOS и Android реализуют его по-разному. В UIKit строим кастомный UIView с CALayer-масками или собираем из пяти UIImageView со статами .full, .half, .empty. В Jetpack Compose — Row с Icon и вычислением через floor/ceil дробного значения. Анимацию заполнения при первой загрузке делаем через withAnimation (Compose) или UIView.animate с изменением ширины clip-mask.
Пагинация и infinite scroll в списке отзывов
Классический OFFSET/LIMIT работает плохо при большом числе отзывов — на 10 000-й странице база всё равно сканирует весь индекс до нужного смещения. Используем cursor-based pagination: сортируем по created_at DESC, id DESC, в ответе возвращаем next_cursor (base64 от последнего id + timestamp), следующий запрос передаёт его как параметр. Cursor-based pagination работает в 10 раз быстрее OFFSET/LIMIT на выборках от 10 000 записей.
На iOS список строим на UICollectionView с UICollectionViewDiffableDataSource — добавление новой страницы через applySnapshot без мерцания. prefetchDataSource запрашивает следующую страницу когда до конца остаётся 3-4 ячейки. На Android — LazyColumn с LazyPagingItems из Paging 3.
Фото к отзыву
Загрузка фото напрямую через основной API — антипаттерн. Правильная схема: клиент запрашивает presigned URL у S3-совместимого хранилища (AWS S3, Cloudflare R2, MinIO), загружает файл напрямую туда, затем передаёт в API только ключ объекта. Сжатие перед загрузкой — на клиенте: iOS через UIImage.jpegData(compressionQuality: 0.75), Android через Bitmap.compress(Bitmap.CompressFormat.JPEG, 75, outputStream). Лимит — 2-3 фото, максимум 5 МБ на файл после сжатия.
Отображение — через Kingfisher (iOS) или Coil (Android) с placeholder и crossfade 200ms. Для галереи при тапе — модальный UIPageViewController или HorizontalPager в Compose с пинч-зумом.
Как устроена полная реализация
Структура данных. Отзыв содержит: user_id, entity_id (продукт, услуга), entity_type, rating (1-5), body (текст, опционально), photos[], status (pending/approved/rejected), helpful_count, created_at. Индексы: (entity_id, entity_type, status, created_at DESC) для выборки одобренных отзывов по объекту.
Модерация. Автоматический pre-filter через профанити-фильтр (библиотека bad-words или кастомный список на бэке) + флаг на ручную проверку для отзывов с ключевыми словами. Фото проходят через AWS Rekognition Moderation Labels или Google Cloud Vision SafeSearch перед публикацией. В панели модератора — очередь с approve/reject и возможностью ответить на отзыв.
Ответ на отзыв. Бизнес отвечает на отзыв — это отдельная сущность review_reply (один к одному с review). При публикации ответа — push-уведомление автору через FCM/APNs с deeplink на отзыв.
Голосование «полезно». helpful_votes — отдельная таблица (user_id, review_id, UNIQUE). Лимит: один голос с одного аккаунта. На клиенте — оптимистичное обновление счётчика с откатом при ошибке.
Верификация покупки. Если платформа позволяет — отмечаем отзывы от реальных покупателей значком «Подтверждённая покупка», проверяя наличие закрытого заказа с user_id и entity_id.
| Компонент | Технология | Особенность |
|---|---|---|
| Звёздный рейтинг | SwiftUI / Jetpack Compose | Денормализованное поле, триггер обновления |
| Пагинация | cursor-based | Стабильная скорость при 1M+ записей |
| Модерация | bad-words + AWS Rekognition | Автоматический pre-filter + ручная очередь |
| Фото | presigned URL + S3/Coil/Kingfisher | Сжатие на клиенте до 5 МБ |
| Ответы бизнеса | review_reply, push-уведомления | FCM/APNs с deeplink |
| Голосование | UNIQUE (user, review) | Оптимистичное обновление |
Почему cursor-based pagination лучше OFFSET/LIMIT?
Cursor-based pagination гарантирует стабильную скорость независимо от количества страниц. При 1 000 000 отзывов запрос с курсором выполняется за те же миллисекунды, что и на первой странице. Исследование PostgreSQL показывает, что OFFSET на больших выборках приводит к полному сканированию индекса до смещения. На мобиле это критично — пользователь не должен ждать подгрузки отзывов больше 200 мс.
Как организована модерация фото?
Автоматическая проверка через AWS Rekognition Moderation Labels или Google Cloud Vision SafeSearch выявляет NSFW-контент. В случае срабатывания — отзыв помечается на ручную проверку. Модератор в панели видит фото и текст, может approve или reject. Это снижает нагрузку на команду и исключает появление нежелательного контента.
Этапы работы
- Аудит текущей реализации (если есть).
- Проектирование схемы данных и API.
- Разработка бэкенд-части.
- Мобильный UI (обе платформы или одна).
- Интеграция модерации.
- Тестирование нагрузкой (Artillery/k6 на сценарий «500 одновременных отзывов»).
- Публикация.
Для Flutter-проектов весь UI — один раз, логика вынесена в ReviewBloc (BLoC) или ReviewNotifier (Riverpod).
Что входит в работу
- Проектирование схемы данных (ER-диаграмма, описание индексов)
- REST/GraphQL API с документацией (Swagger/GraphQL Playground)
- Мобильный UI под iOS/Android или Flutter
- Интеграция модерации (автоматическая + ручная)
- Нагрузочное тестирование и оптимизация
- Доступ к репозиторию, CI/CD-пайплайн
- Обучение команды заказчика (1 сессия)
- Поддержка 2 недели после релиза
Сроки
Базовая система (звёздный рейтинг, текстовый отзыв, список с пагинацией, модерация через статус) — 3-5 рабочих дней. С фотографиями, ответами бизнеса, голосованием и верификацией покупки — 8-12 дней. Стоимость рассчитывается индивидуально после анализа требований.
Типичные ошибки при реализации
- Считать рейтинг на лету без кэша - Использовать OFFSET/LIMIT для пагинации - Не сжимать фото перед загрузкой - Пропустить модерацию фото с NSFW-контентом - Не добавить уникальность голосовОбратитесь к нам — мы реализовали 20+ систем отзывов для маркетплейсов и сервисов более 5 лет. Оценим ваш проект за 1 день. Получите консультацию сейчас.







