Реализация Presence-индикатора (кто сейчас онлайн) на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Presence-индикатора (кто сейчас онлайн) на сайте
Простая
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы

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

Этапы разработки

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

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация Presence-индикатора (кто сейчас онлайн) на сайте

Зелёная точка рядом с аватаром — простая вещь внешне, но за ней стоит конкретная инфраструктурная задача: сервер должен знать, кто подключён прямо сейчас, и уведомлять других пользователей при изменении этого состояния. Применения: чат поддержки, профили пользователей, списки участников курса, совместное редактирование.

Как определяется «онлайн»

Три подхода, разные по точности:

Heartbeat через WebSocket/SSE — наиболее точный. Пока соединение активно, пользователь онлайн. При разрыве — событие disconnect. Задержка определения офлайн: секунды.

Heartbeat через HTTP — клиент отправляет POST /api/presence/ping каждые 30 секунд. Если пинга не было дольше 60 секунд — пользователь считается офлайн. Задержка определения: до 60 секунд. Проще в реализации, не требует постоянного соединения.

Last seen — мягкий вариант. Не «онлайн/офлайн», а «был 3 минуты назад». Обновляется при любом запросе к API. Полезно для приватности (пользователь сам выбирает показывать ли точный статус).

Для большинства сайтов достаточно heartbeat через HTTP — не нужно держать WebSocket-соединение ради одной точки.

Redis-хранилище присутствия

Присутствие хранится в Redis, не в PostgreSQL. Причина: частые записи (каждые 30 секунд на пользователя), TTL-логика, нет смысла в персистентности.

class PresenceService
{
    private const TTL = 90; // секунды без пинга = офлайн

    public function markOnline(int $userId, string $context = 'global'): void
    {
        Redis::setex("presence:{$context}:{$userId}", self::TTL, now()->timestamp);

        // Уведомить канал, если это первое появление
        $wasOnline = Redis::exists("presence_flag:{$context}:{$userId}");
        if (!$wasOnline) {
            Redis::setex("presence_flag:{$context}:{$userId}", self::TTL + 10, 1);
            broadcast(new UserCameOnline($userId, $context));
        }
    }

    public function markOffline(int $userId, string $context = 'global'): void
    {
        Redis::del("presence:{$context}:{$userId}");
        Redis::del("presence_flag:{$context}:{$userId}");
        broadcast(new UserWentOffline($userId, $context));
    }

    public function getOnlineUsers(string $context = 'global'): array
    {
        $keys = Redis::keys("presence:{$context}:*");
        return array_map(fn($k) => (int) last(explode(':', $k)), $keys);
    }

    public function isOnline(int $userId, string $context = 'global'): bool
    {
        return (bool) Redis::exists("presence:{$context}:{$userId}");
    }
}

Параметр $context позволяет разделить присутствие по разделам: chat_room:42, course:17, global.

Heartbeat-эндпоинт

Route::middleware('auth:sanctum')->post('/api/presence/ping', function (Request $request) {
    app(PresenceService::class)->markOnline(
        $request->user()->id,
        $request->input('context', 'global')
    );
    return response()->json(['ok' => true]);
});

Клиент вызывает этот эндпоинт при загрузке страницы и далее каждые 30 секунд:

const ping = () => fetch('/api/presence/ping', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
    body: JSON.stringify({ context: 'global' }),
});

ping();
const interval = setInterval(ping, 30_000);

// Очистка при закрытии вкладки
window.addEventListener('beforeunload', () => {
    clearInterval(interval);
    navigator.sendBeacon('/api/presence/offline'); // fire-and-forget
});

navigator.sendBeacon — единственный надёжный способ отправить запрос при закрытии вкладки. Обычный fetch в beforeunload браузер может прервать.

Broadcast событий

class UserCameOnline implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public readonly int    $userId,
        public readonly string $context,
    ) {}

    public function broadcastOn(): Channel
    {
        return new Channel("presence.{$this->context}");
    }
}

Индикатор в интерфейсе

// Начальное состояние загружается вместе со страницей
const onlineUsers = new Set(initialOnlineUserIds);

function updateDot(userId, isOnline) {
    const dot = document.querySelector(`[data-user-id="${userId}"] .presence-dot`);
    if (!dot) return;
    dot.classList.toggle('bg-green-500', isOnline);
    dot.classList.toggle('bg-gray-300', !isOnline);
    dot.title = isOnline ? 'Онлайн' : 'Офлайн';
}

Echo.channel('presence.global')
    .listen('UserCameOnline', ({ userId }) => {
        onlineUsers.add(userId);
        updateDot(userId, true);
    })
    .listen('UserWentOffline', ({ userId }) => {
        onlineUsers.delete(userId);
        updateDot(userId, false);
    });

Присутствие через Laravel Presence Channels

Если используется Laravel Echo + Pusher/Reverb, можно использовать встроенный механизм Presence Channels — он автоматически управляет списком подключённых пользователей:

Echo.join('room.42')
    .here((users) => { /* начальный список */ })
    .joining((user) => updateDot(user.id, true))
    .leaving((user) => updateDot(user.id, false));

Бэкенд просто авторизует канал и возвращает данные пользователя:

Broadcast::channel('room.{roomId}', function (User $user, int $roomId) {
    if ($user->canAccessRoom($roomId)) {
        return ['id' => $user->id, 'name' => $user->name, 'avatar' => $user->avatar_url];
    }
});

Сроки

  • Heartbeat-пинг + Redis TTL + индикатор: 1–2 дня
  • Broadcast при смене статуса (UserCameOnline / Offline): 1 день
  • Presence Channels через Laravel Echo: 1 день
  • Last seen вместо онлайн/офлайн: 0.5 дня
  • Настройки приватности (скрыть статус): +0.5 дня