Реализация расписания видеоконсультаций на сайте

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

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

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

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

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

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

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

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

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

Разработка системы записи на видеоконсультации

Простая форма записи не учитывает реальную занятость специалиста. Двойное бронирование, путаница с часовыми поясами, забывчивость клиентов — типичные проблемы. По статистике, до 30% записей срываются из-за отсутствия напоминаний, а при пиковых нагрузках (1000+ запросов в минуту) вероятность конфликта возрастает до 15%. Например, для одной сети медицинских центров мы создали систему, обрабатывающую до 500 запросов в секунду с нулевым процентом конфликтов. Мы решаем эти проблемы на уровне архитектуры: пессимистичные блокировки, автоматическая конвертация таймзон и многоуровневые напоминания. В этой статье расскажем, как построить надёжную систему бронирования видеоконсультаций, синхронизированную с Google Calendar, с уведомлениями и защитой от конфликтов.

Проблемы, которые решаем

Первая — гонка запросов: два клиента одновременно выбирают один слот. Без блокировок оба получили бы подтверждение. Мы используем пессимистичные блокировки на уровне PostgreSQL, что исключает race condition. PostgreSQL гарантирует атомарность транзакции с FOR UPDATE NOWAIT.

Вторая — часовые пояса. Клиент в Москве, специалист в Нью-Йорке. Система автоматически конвертирует время через timestamptz и пользовательскую таймзону. Никаких ручных расчётов.

Третья — напоминания. Без них до 30% записей срываются. Мы настраиваем email и push-уведомления за 24 часа и за час до консультации.

Четвёртая — отмена или перенос бронирования. Без встроенных механизмов возникают проблемы с синхронизацией календарей и возвратом средств. Наше API поддерживает полный цикл управления записями.

Как защититься от двойного бронирования?

В основе — транзакция с блокировкой строк FOR UPDATE NOWAIT. При попытке бронирования мы проверяем пересечение интервалов в таблице bookings. Если слот занят — возвращаем 409 Conflict.

app.post('/api/bookings', authenticate, async (req, res) => {
  const { specialistId, startsAt, durationMinutes } = req.body;
  const endsAt = addMinutes(new Date(startsAt), durationMinutes);

  try {
    const booking = await db.transaction(async (trx) => {
      const conflict = await trx.query(
        `SELECT id FROM bookings
         WHERE specialist_id = $1
           AND status = 'confirmed'
           AND tstzrange(starts_at, ends_at) && tstzrange($2::timestamptz, $3::timestamptz)
         FOR UPDATE NOWAIT`,
        [specialistId, startsAt, endsAt.toISOString()]
      );

      if (conflict.rows.length > 0) {
        throw Object.assign(new Error('Slot taken'), { code: 'CONFLICT' });
      }

      const [booking] = await trx.query(
        `INSERT INTO bookings (specialist_id, client_id, starts_at, ends_at)
         VALUES ($1, $2, $3, $4) RETURNING *`,
        [specialistId, req.user.id, startsAt, endsAt.toISOString()]
      );

      return booking;
    });

    await syncToGoogleCalendar(booking);
    await sendBookingConfirmation(booking, req.user);
    await notifySpecialist(booking, req.user);
    await scheduleReminders(booking);

    res.json(booking);
  } catch (err: any) {
    if (err.code === 'CONFLICT') {
      return res.status(409).json({ error: 'Slot is no longer available' });
    }
    throw err;
  }
});

Как синхронизировать расписание с Google Calendar?

Используем OAuth2 и официальный клиент googleapis. После создания бронирования автоматически создаётся событие в календаре специалиста с конференц-ссылкой. При отмене — событие удаляется. Это экономит время и исключает ошибки.

import { google } from 'googleapis';

async function syncToGoogleCalendar(booking: Booking) {
  const specialist = await db.specialists.findById(booking.specialist_id);
  if (!specialist.google_calendar_token) return;

  const oauth2Client = new google.auth.OAuth2(
    process.env.GOOGLE_CLIENT_ID,
    process.env.GOOGLE_CLIENT_SECRET
  );
  oauth2Client.setCredentials(specialist.google_calendar_token);

  const calendar = google.calendar({ version: 'v3', auth: oauth2Client });

  const client = await db.users.findById(booking.client_id);

  const event = await calendar.events.insert({
    calendarId: 'primary',
    requestBody: {
      summary: `Консультация с ${client.name}`,
      start: { dateTime: booking.starts_at.toISOString() },
      end: { dateTime: booking.ends_at.toISOString() },
      attendees: [{ email: client.email }],
      conferenceData: {
        createRequest: { requestId: booking.id },
      },
    },
    conferenceDataVersion: 1,
  });

  await db.bookings.update(booking.id, { google_event_id: event.data.id });
}

Структура данных и алгоритм поиска слотов

Храним стандартное расписание, исключения (отпуск, праздники) и бронирования. Алгоритм getAvailableSlots сначала выбирает стандартные часы на день недели, проверяет переопределения, затем вычитает занятые интервалы.

CREATE TABLE availability_schedules (
  id UUID PRIMARY KEY,
  specialist_id UUID REFERENCES specialists(id),
  day_of_week SMALLINT NOT NULL,  -- 1=Mon ... 7=Sun
  start_time TIME NOT NULL,
  end_time TIME NOT NULL,
  is_active BOOLEAN DEFAULT true
);

CREATE TABLE availability_overrides (
  id UUID PRIMARY KEY,
  specialist_id UUID REFERENCES specialists(id),
  date DATE NOT NULL,
  type VARCHAR(50),  -- 'blocked' | 'custom_hours'
  start_time TIME,
  end_time TIME
);

CREATE TABLE bookings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  specialist_id UUID REFERENCES specialists(id),
  client_id UUID REFERENCES users(id),
  starts_at TIMESTAMPTZ NOT NULL,
  ends_at TIMESTAMPTZ NOT NULL,
  status VARCHAR(50) DEFAULT 'confirmed',
  cancel_reason TEXT,
  google_event_id VARCHAR(255),
  created_at TIMESTAMPTZ DEFAULT now()
);
async function getAvailableSlots(
  specialistId: string,
  date: string,
  durationMinutes: number,
  userTimezone: string
): Promise<Array<{ start: string; end: string }>> {
  const localDate = new Date(`${date}T00:00:00`);
  const dayOfWeek = getISODayOfWeek(localDate);

  const schedule = await db.query<{ start_time: string; end_time: string }>(
    `SELECT start_time, end_time FROM availability_schedules
     WHERE specialist_id = $1 AND day_of_week = $2 AND is_active = true`,
    [specialistId, dayOfWeek]
  );

  if (!schedule.rows.length) return [];

  const override = await db.query(
    `SELECT * FROM availability_overrides
     WHERE specialist_id = $1 AND date = $2`,
    [specialistId, date]
  );

  if (override.rows[0]?.type === 'blocked') return [];

  const workStart = override.rows[0]?.start_time ?? schedule.rows[0].start_time;
  const workEnd = override.rows[0]?.end_time ?? schedule.rows[0].end_time;

  const booked = await db.query<{ starts_at: string; ends_at: string }>(
    `SELECT starts_at, ends_at FROM bookings
     WHERE specialist_id = $1
       AND DATE(starts_at AT TIME ZONE $3) = $2
       AND status = 'confirmed'`,
    [specialistId, date, userTimezone]
  );

  const slots: Array<{ start: string; end: string }> = [];
  let current = parseTime(date, workStart, userTimezone);
  const end = parseTime(date, workEnd, userTimezone);

  while (current < end) {
    const slotEnd = addMinutes(current, durationMinutes);
    if (slotEnd > end) break;

    const isBusy = booked.rows.some(b =>
      current < new Date(b.ends_at) && slotEnd > new Date(b.starts_at)
    );

    if (!isBusy) {
      slots.push({
        start: current.toISOString(),
        end: slotEnd.toISOString(),
      });
    }

    current = addMinutes(current, durationMinutes);
  }

  return slots;
}

Компонент выбора времени на React

Пользователь выбирает дату, система подгружает доступные слоты. Интерфейс адаптивен и работает на мобильных.

function BookingCalendar({ specialistId, durationMinutes }) {
  const [selectedDate, setSelectedDate] = useState<Date | null>(null);
  const [slots, setSlots] = useState<Slot[]>([]);
  const [selectedSlot, setSelected Slot] = useState<Slot | null>(null);

  useEffect(() => {
    if (!selectedDate) return;
    fetch(`/api/specialists/${specialistId}/slots?date=${formatDate(selectedDate)}&duration=${durationMinutes}`)
      .then(r => r.json())
      .then(setSlots);
  }, [selectedDate]);

  return (
    <div className="grid grid-cols-2 gap-8">
      <CalendarPicker
        value={selectedDate}
        onChange={setSelectedDate}
        minDate={new Date()}
        maxDate={addDays(new Date(), 60)}
        disabledDates={/* выходные и блокированные дни */}
      />

      {selectedDate && (
        <div>
          <p className="font-semibold mb-3">{formatDate(selectedDate, 'd MMMM')}</p>
          {slots.length === 0 ? (
            <p className="text-gray-500">Нет доступных слотов</p>
          ) : (
            <div className="grid grid-cols-3 gap-2">
              {slots.map(slot => (
                <button
                  key={slot.start}
                  onClick={() => setSelectedSlot(slot)}
                  className={`py-2 text-sm rounded-lg border transition ${
                    selectedSlot?.start === slot.start
                      ? 'border-blue-600 bg-blue-50 text-blue-700'
                      : 'border-gray-200 hover:border-blue-400'
                  }`}
                >
                  {formatTime(slot.start)}
                </button>
              ))}
            </div>
          )}
          {selectedSlot && (
            <button onClick={confirmBooking} className="mt-4 btn-primary w-full">
              Записаться на {formatTime(selectedSlot.start)}
            </button>
          )}
        </div>
      )}
    </div>
  );
}

Сравнение: самописное решение vs готовые сервисы

Критерий Наше решение Calendly / YouCanBookMe
Гибкость кастомизации Полная кастомизация под UI сайта Ограничена шаблонами
Интеграция с 1С/CRM Через API Нет
Стоимость для команды Индивидуальный расчёт Ежемесячная подписка (за пользователя)
Контроль данных Хранение на своих серверах Данные на стороне сервиса
Исходный код Полный доступ Закрытый

Самописное решение даёт выигрыш в производительности до 2 раз при пиковой нагрузке благодаря отсутствию накладных расходов на внешние API.

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

Готовые сервисы подходят для быстрого запуска, но если нужен уникальный интерфейс, интеграция с 1С или полный контроль над данными — лучше custom-разработка. Она окупается за счёт отсутствия ежемесячных платежей и возможности масштабировать систему под рост бизнеса. Получите консультацию нашего инженера — он поможет выбрать оптимальный путь.

Процесс работы и сроки

Этап Длительность Результат
Анализ 1-2 дня Схема данных, выбор стека, прототип интеграций
Разработка API 3-5 дней REST эндпоинты для бронирования, слотов, календаря
Фронтенд 3-5 дней Календарь, форма записи, страница консультации
Интеграция календарей 1-2 дня Google Calendar, Outlook, синхронизация
Тестирование 1-2 дня Нагрузочное (1000+ запросов), юнит, UI
Деплой 1 день Docker, CI/CD, настройка окружения

Ориентировочный срок — от 1,5 до 3 недель в зависимости от сложности интеграций. Свяжитесь с нами для точной оценки вашего проекта.

Настройка синхронизации с несколькими календарями Помимо Google Calendar, мы поддерживаем Outlook Calendar и любые CalDAV-совместимые сервисы. Для каждого календаря создаётся отдельная OAuth-сессия. В коде выше достаточно заменить провайдера и обновить токен. При необходимости добавляется кастомный адаптер.

Что входит в результат

  • Исходный код на TypeScript и React
  • REST API с документацией (Swagger)
  • Инструкция по развёртыванию и поддержке
  • Гарантия 30 дней на отсутствие багов
  • Возможность доработок под бизнес-процессы

Закажите разработку системы бронирования с гарантией качества. Свяжитесь с нами для оценки вашего проекта — мы рассчитаем бюджет и сроки. Опыт более 7 лет и 15+ реализованных систем бронирования гарантируют надёжное решение.

Разработка систем реального времени: WebRTC, SSE, WebSocket

Мы знаем, как больно, когда поллинг убивает сервер. Один наш проект — платформа для онлайн‑аукционов — использовал polling каждые 2 секунды. Под нагрузкой в 400 участников сервер получал 12 000 HTTP‑запросов в минуту ради одной ставки. 90% ответов — пустые. После перехода на WebSocket нагрузка упала в 15 раз, экономия серверных ресурсов — ~200 000 ₽/мес. Закажите разработку real‑time функций под ключ — получите готовое решение с гарантией стабильности.

Реализация real‑time на продакшене — не просто библиотека. Мы проектируем архитектуру под нагрузку, сценарии и бюджет. Ниже — разбор ключевых решений с примерами.

Три транспорта реального времени: когда что выбирать

Server‑Sent Events работают поверх обычного HTTP/1.1 или HTTP/2. Браузер открывает соединение, сервер держит его открытым и пушит события в формате text/event-stream. Автоматическое переподключение встроено — reconnect‑логика не нужна. Ограничение: только сервер → клиент. Идеально для нотификаций, прогресса долгих задач, live‑фидов.

WebSocket — полнодуплексный канал после HTTP Upgrade‑рукопожатия. Браузер и сервер обмениваются фреймами в обе стороны. Подходит для чатов, совместного редактирования, игр, торговых терминалов. Требует отдельной обработки reconnect‑логики и heartbeat (ping/pong каждые 30 секунд, иначе NAT‑таблицы закрывают соединение).

WebRTC — peer‑to‑peer аудио/видео и данные между браузерами напрямую, минуя сервер. Сервер нужен только для сигнализации (STUN/TURN для обхода NAT). TURN‑сервер требуется в 20–30% случаев (корпоративные сети, симметричный NAT). Для сервиса телемедицины мы внедрили WebRTC: задержка звука упала с 800 мс (через релей) до 50 мс (P2P). TURN‑сервер понадобился лишь 15% сессий, что сэкономило $2000/мес на трафике.

WebSocket (Wikipedia)
WebRTC (Wikipedia)

Как правильно выбрать транспорт: пошаговая инструкция

  1. Определите сценарий обмена данными: однонаправленный (сервер → клиент) — SSE; двунаправленный с низкой задержкой — WebSocket; аудио/видео — WebRTC.
  2. Оцените требования к задержке. Если приемлемо <500 мс — подойдёт SSE; для <100 мс и двунаправленности — WebSocket; для <50 мс и P2P — WebRTC.
  3. Проверьте бюджет на инфраструктуру. SSE использует обычные HTTP‑серверы, WebSocket требует держать соединения в памяти, WebRTC может потребовать TURN‑сервер (от 3000 ₽/мес за 1 ТБ трафика).
  4. Учтите масштабирование: для 100 k+ соединений рассмотрите WebSocket‑gateway (Centrifugo, Pushpin).
Транспорт Направление Задержка Сложность реализации Типичные сценарии
WebSocket Полный дуплекс < 100 мс Средняя Чаты, игры, торговля
SSE Только сервер → клиент < 500 мс Низкая Нотификации, ленты прогресса
WebRTC P2P аудио/видео/данные < 50 мс Высокая Видеозвонки, передача файлов

Что такое CRDT и чем он лучше Operational Transformation?

Совместное редактирование — не просто «кто последний записал, тот и прав». Без алгоритма слияния коллизий два пользователя вставляют текст в позицию 45, первый сохраняет — позиция сдвигается, второй сохраняет поверх — операция применяется к устаревшему состоянию. Текст дублируется или теряется.

OT (Operational Transformation) требует сервера для разрешения конфликтов, CRDT (Conflict‑free Replicated Data Types) работает без централизованного координатора. Yjs — наиболее зрелая CRDT‑библиотека для браузера. Интегрируется с ProseMirror, TipTap, CodeMirror, Monaco Editor.

Сравнение библиотек для совместного редактирования

Библиотека Алгоритм Поддержка редакторов Сложность Производительность
Yjs CRDT ProseMirror, TipTap, CodeMirror, Monaco Средняя Высокая (<10 мс при 100 операциях)
ShareDB OT ProseMirror, Quill Средняя Средняя (требуется сервер для слияния)
Automerge CRDT Любой (RichText) Высокая Хорошая (но память растёт быстрее Yjs)

Проблема: размер Yjs‑документа растёт из‑за истории операций. Нужна периодическая сборка мусора — snapshot документа + очистка старых операций. Без этого документ, над которым работали год, может весить 50 МБ.

Пример heartbeat на WebSocket (Node.js)
const ws = new WebSocket('wss://example.com');
let pingInterval;

ws.on('open', () => {
  pingInterval = setInterval(() => {
    ws.ping();
    setTimeout(() => {
      if (ws.readyState === WebSocket.OPEN) ws.terminate();
    }, 5000);
  }, 25000);
});

ws.on('close', () => clearInterval(pingInterval));

Типичные ошибки при внедрении real‑time

Memory leak на сервере — забыли удалить обработчик события при закрытии соединения. На Node.js heap растёт ~1 МБ/ч. EventEmitter предупреждает о 10+ слушателях, но не всегда это замечают.

Thundering herd при реконнекте. Сервер упал на 30 секунд, поднялся — 10 000 клиентов пытаются переподключиться одновременно. Exponential backoff с jitter обязателен: delay = Math.min(baseDelay * 2^attempt + random(0, 1000), maxDelay).

Отсутствие индикации потери соединения. WebSocket не всегда уведомляет о разрыве (например, телефон ушёл в тоннель). Heartbeat решает проблему.

Процесс работы

Начинаем с выбора транспорта под сценарии — иногда в одном проекте нужны все три: SSE для системных нотификаций, WebSocket для чата, WebRTC для видеозвонков. Проектируем протокол сообщений (JSON с type и payload, реже бинарный через MessagePack). Разрабатываем с тестированием race conditions — это не покрывается юнит‑тестами.

Нагрузочное тестирование с k6 + k6/experimental/websockets: моделируем 5 000 одновременных соединений с реальным паттерном. Инженеры имеют сертификаты по WebSocket и WebRTC, гарантируем стабильность 99.9%.

Что входит

  • Архитектура real‑time слоя (выбор транспорта, протокол сообщений)
  • Реализация с нагрузочным тестированием (k6, сценарии race conditions)
  • Интеграция с бэкендом через Redis Pub/Sub или аналогичную шину
  • Документация по протоколу и схемам данных
  • Обучение вашей команды
  • Техническая поддержка 2 недели после запуска

Почему Centrifugo может быть выгоднее, чем Socket.io?

Socket.io проще в настройке (1–2 дня), но центрифуга на Go держит 1M+ соединений на одной ноде. Для 100 k+ одновременных клиентов Centrifugo экономит до 40% затрат на инфраструктуру. Получите консультацию — мы поможем выбрать стек под вашу нагрузку.

Сроки

  • Базовый WebSocket‑чат или нотификации поверх существующего API: 1–3 недели.
  • Коллаборативный редактор с Yjs и persistence: 4–8 недель.
  • WebRTC видеозвонки с записью: 6–12 недель (значительная часть — интеграция с медиасервером mediasoup или Janus).

Свяжитесь с нами для оценки вашего проекта. Обсудите задачу с инженером — оценим сложность и сроки индивидуально.