Разработка системы отзывов и рейтингов в мобильном приложении
Без системы отзывов приложение теряет один из главных инструментов социального доказательства — пользователи не видят чужого опыта и не оставляют своего. Но сделать её правильно сложнее, чем кажется: агрегированный рейтинг с перекосом из-за нескольких ранних отзывов, фотографии к отзыву, которые грузятся 4 секунды, или модерация, которую обходят ботами — всё это типичные проблемы, с которыми приходят на доработку.
Что обычно ломается в самописных реализациях
Агрегация и обновление рейтинга
Самая частая ошибка — считать средний рейтинг на лету 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), следующий запрос передаёт его как параметр.
На 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.
Этапы работы
Аудит текущей реализации (если есть) → проектирование схемы данных и API → разработка бэкенд-части → мобильный UI (обе платформы или одна) → интеграция модерации → тестирование нагрузкой (Artillery/k6 на сценарий «500 одновременных отзывов») → публикация.
Для Flutter-проектов весь UI — один раз, логика вынесена в ReviewBloc (BLoC) или ReviewNotifier (Riverpod).
Сроки
Базовая система (звёздный рейтинг, текстовый отзыв, список с пагинацией, модерация через статус) — 3-5 рабочих дней. С фотографиями, ответами бизнеса, голосованием и верификацией покупки — 8-12 дней. Стоимость рассчитывается индивидуально после анализа требований.







