Разработка системы лайков и рейтингов для сайта

Когда тысячи пользователей одновременно голосуют на сайте, обычный счётчик не справляется: данные дублируются, а база падает. Мы разрабатываем системы лайков и рейтингов, которые выдерживают пиковые нагрузки и защищены от накрутки. Наша команда реализует проект под ключ — от архитектуры до внедрения, обеспечивая стабильную работу и последующую поддержку.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка системы лайков и рейтингов для сайта
Средний
~2-3 дня
Часто задаваемые вопросы

Представьте: на сайте 5000 пользователей одновременно лайкают одну статью. Счётчик сошёл с ума, записи дублируются, а база падает под deadlock. Это не гипотетика — мы видели такое на продакшене. Система лайков и рейтингов — не просто «кнопка сердечка», а инженерная задача с атомарностью, кэшированием и защитой от накрутки. Мы разрабатываем такие системы под ключ уже более пяти лет, и вот как это делаем.

Почему система лайков сложнее, чем кажется?

На первый взгляд — связка «кнопка + счётчик». Но при параллельных запросах без блокировок значение счётчика может разойтись с реальным количеством голосов. Ещё одна проблема — дублирование: один пользователь может поставить лайк много раз, если не поставить уникальный constraint. Наконец, скорость: если каждый лайк пишется в БД и пересчитывается, при пиковой нагрузке страница будет тормозить. Мы решаем это комбинацией денормализации, кэширования и оптимистичных обновлений на фронтенде.

Как защитить систему от накрутки?

Защита строится на нескольких уровнях. На уровне базы — уникальный индекс (user_id, likeable_id, likeable_type) гарантирует один голос от пользователя. На уровне API — rate limiting: не более 60 запросов в минуту на одного пользователя. Для гостей используем ограничение по IP с куками. В кэше храним факт голосования на 5 минут, чтобы не дёргать БД. Всё это вместе делает накрутку практически невозможной.

Структура базы данных

-- Универсальная таблица лайков (polymorphic)
CREATE TABLE likes (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    likeable_id INTEGER NOT NULL,
    likeable_type VARCHAR(50) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (user_id, likeable_id, likeable_type)
);
CREATE INDEX ON likes(likeable_type, likeable_id);

-- Рейтинги
CREATE TABLE ratings (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    ratable_id INTEGER NOT NULL,
    ratable_type VARCHAR(50) NOT NULL,
    value SMALLINT NOT NULL CHECK (value BETWEEN 1 AND 5),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (user_id, ratable_id, ratable_type)
);

-- Счётчики в основных таблицах (денормализация для производительности)
ALTER TABLE articles ADD COLUMN likes_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE products ADD COLUMN rating_avg NUMERIC(3,2) NOT NULL DEFAULT 0;
ALTER TABLE products ADD COLUMN ratings_count INTEGER NOT NULL DEFAULT 0;

Laravel: лайки

trait Likeable {
    public function likes(): MorphMany {
        return $this->morphMany(Like::class, 'likeable');
    }

    public function isLikedBy(?User $user): bool {
        if (!$user) return false;

        return Cache::remember(
            "liked:{$this->getMorphClass()}:{$this->id}:{$user->id}",
            300,
            fn() => $this->likes()->where('user_id', $user->id)->exists()
        );
    }
}

class LikeController extends Controller {
    public function toggle(Request $request, string $type, int $id): JsonResponse {
        $model = $this->resolveModel($type, $id);
        $user = $request->user();

        $existing = Like::where([
            'user_id' => $user->id,
            'likeable_type' => $type,
            'likeable_id' => $id,
        ])->first();

        if ($existing) {
            $existing->delete();
            $model->decrement('likes_count');
            $liked = false;
        } else {
            Like::create([
                'user_id' => $user->id,
                'likeable_type' => $type,
                'likeable_id' => $id,
            ]);
            $model->increment('likes_count');
            $liked = true;
        }

        Cache::forget("liked:{$type}:{$id}:{$user->id}");

        return response()->json([
            'liked' => $liked,
            'count' => $model->fresh()->likes_count,
        ]);
    }

    private function resolveModel(string $type, int $id): Model {
        return match ($type) {
            'article' => Article::findOrFail($id),
            'comment' => Comment::findOrFail($id),
            'product' => Product::findOrFail($id),
            default => abort(400, "Unknown type: {$type}"),
        };
    }
}

Рейтинги (звёзды)

class RatingController extends Controller
{
    public function store(Request $request, string $type, int $id): JsonResponse
    {
        $request->validate(['value' => 'required|integer|between:1,5']);

        $model = $this->resolveModel($type, $id);

        Rating::updateOrCreate(
            [
                'user_id' => $request->user()->id,
                'ratable_type' => $type,
                'ratable_id' => $id,
            ],
            ['value' => $request->value]
        );

        $stats = Rating::where(['ratable_type' => $type, 'ratable_id' => $id])
            ->selectRaw('AVG(value) as avg, COUNT(*) as cnt')
            ->first();

        $model->update([
            'rating_avg' => round($stats->avg, 2),
            'ratings_count' => $stats->cnt,
        ]);

        return response()->json([
            'user_rating' => $request->value,
            'avg' => round($stats->avg, 1),
            'count' => $stats->cnt,
            'distribution' => Rating::where(['ratable_type' => $type, 'ratable_id' => $id])
                ->groupBy('value')
                ->selectRaw('value, COUNT(*) as count')
                ->pluck('count', 'value'),
        ]);
    }
}

React: UI компоненты

// Лайк-кнопка
function LikeButton({ type, id, initialCount, initialLiked }: LikeButtonProps) {
  const [liked, setLiked] = useState(initialLiked);
  const [count, setCount] = useState(initialCount);
  const [loading, setLoading] = useState(false);

  const toggle = async () => {
    if (loading) return;
    setLoading(true);
    setLiked(!liked);
    setCount(c => liked ? c - 1 : c + 1);
    try {
      const { data } = await api.post(`/api/likes/${type}/${id}/toggle`);
      setLiked(data.liked);
      setCount(data.count);
    } catch {
      setLiked(liked);
      setCount(count);
    } finally {
      setLoading(false);
    }
  };

  return (
    <button
      onClick={toggle}
      className={`like-btn ${liked ? 'like-btn--active' : ''}`}
      aria-label={liked ? 'Убрать лайк' : 'Поставить лайк'}
      aria-pressed={liked}
    >
      <HeartIcon filled={liked} />
      <span>{count.toLocaleString('ru-RU')}</span>
    </button>
  );
}

// Звёздный рейтинг
function StarRating({ type, id, userRating, avgRating, ratingsCount }: StarRatingProps) {
  const [hover, setHover] = useState(0);
  const [selected, setSelected] = useState(userRating || 0);

  const handleRate = async (value: number) => {
    setSelected(value);
    await api.post(`/api/ratings/${type}/${id}`, { value });
  };

  return (
    <div className="star-rating">
      <div className="stars" role="radiogroup" aria-label="Оценка">
        {[1, 2, 3, 4, 5].map(star => (
          <button
            key={star}
            role="radio"
            aria-checked={selected === star}
            aria-label={`${star} звезд`}
            className={`star ${star <= (hover || selected) ? 'star--filled' : ''}`}
            onMouseEnter={() => setHover(star)}
            onMouseLeave={() => setHover(0)}
            onClick={() => handleRate(star)}
          >
            ★
          </button>
        ))}
      </div>
      <span className="rating-summary">
        {avgRating.toFixed(1)} ({ratingsCount.toLocaleString('ru-RU')} оценок)
      </span>
    </div>
  );
}

Сравнение подходов: синхронное vs оптимистичное обновление

Критерий Синхронное (ожидание ответа) Оптимистичное (мгновенное)
Восприятие скорости Медленное, задержка 200-500 мс Мгновенное, UX выигрывает в 3 раза
Сложность реализации Низкая Средняя (нужен откат при ошибке)
Надёжность счётчика Абсолютная Возможен временный рассинхрон
Нагрузка на сервер Высокая (каждый клик — запрос) Средняя (та же, но UX не страдает)

Как обеспечить производительность при высоких нагрузках?

Мы используем комбинацию денормализации счётчиков и кэширования в Redis. Для лайков: при каждом голосовании инкрементим likes_count в таблице articles через атомарный UPDATE ... SET likes_count = likes_count + 1 — это быстрее, чем COUNT(*). Для рейтингов: пересчёт агрегатов выполняется только при изменении оценки, а не при каждом просмотре. Кроме того, на фронтенде применяется optimistic update: пользователь видит изменение мгновенно, а запрос уходит асинхронно. При ошибке состояние откатывается. Такой подход снижает нагрузку на сервер в 2–3 раза.

Какую базу данных выбрать для голосов?

PostgreSQL или MySQL?
Критерий PostgreSQL MySQL
Уникальные constraint Поддерживает Поддерживает
Частичные индексы Да (фильтр по status) Нет
JSON-поля для мета Отличная поддержка Ограниченная
Производительность при 1000 RPS Высокая Высокая
Рекомендация Для сложных выборок Для простых схем

Обе СУБД справляются с нагрузкой до 10 000 лайков в секунду при правильной индексации. Мы чаще используем PostgreSQL из-за частичных индексов и лучшей поддержки конкурентных обновлений.

Этапы внедрения системы лайков и рейтингов

  1. Анализ — изучаем вашу модель данных и нагрузку (1 день).
  2. Проектирование — разрабатываем схему БД и API (1–2 дня).
  3. Реализация — пишем бэкенд и фронтенд компоненты (2–4 дня).
  4. Тестирование — нагрузочное тестирование, отлов race conditions (1–2 дня).
  5. Деплой и обучение — разворачиваем на сервере, передаём документацию (1 день).

Что входит в работу

Мы передаём полный комплект: исходный код с комментариями, документацию API (OpenAPI), инструкцию по развёртыванию, доступ к репозиторию, обучение вашей команды (1–2 часа) и поддержку в течение 2 недель после сдачи. Гарантируем, что система пройдёт нагрузочное тестирование при 1000 RPS.

Сроки и стоимость

Базовая система лайков (polymorphic) с React UI и оптимистичными обновлениями — 2–3 дня. Рейтинги 1–5 с агрегатами и распределением — ещё 1–2 дня. Стоимость рассчитывается индивидуально после анализа вашего проекта. Свяжитесь с нами — оценим задачу за один день.

Почему выбирают нас

  • Пятилетний опыт в разработке систем взаимодействия
  • Более 120 реализованных проектов
  • 98% клиентов рекомендуют нас коллегам
  • Используем только проверенные технологии: Laravel, React, PostgreSQL

Гарантируем: система будет работать без ошибок при пиковых нагрузках, а если что-то пойдёт не так — исправим в течение 24 часов.

Наше готовое решение сокращает бюджет в два раза по сравнению с разработкой с нуля, а сроки — в три. Закажите оценку вашего проекта — мы рассчитаем стоимость за один день. Получите консультацию, и мы покажем, как система будет работать на ваших нагрузках.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060