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

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

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация видеоконсультаций (один на один) на сайте
Сложная
~2-4 недели
Часто задаваемые вопросы

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

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

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

  • 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

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

Видеоконсультации — это не просто видеозвонок. Это полноценная система: расписание специалиста, бронирование слота, напоминания, комната ожидания, сам звонок, запись по желанию, история консультаций. Нужен связный пользовательский путь от «выбрать время» до «завершить сеанс».

Архитектура системы

Client booking → Appointment DB → Reminder queue → Video session → Recording → Notes
      ↓                                ↓                 ↓
  Calendar UI              Email/SMS (BullMQ)        LiveKit/Daily
      ↓
  Availability slots
  (specialist schedule)

Модель данных

CREATE TABLE specialists (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES users(id),
  name VARCHAR(255),
  specialty VARCHAR(100),
  timezone VARCHAR(100) DEFAULT 'Europe/Moscow',
  session_duration_minutes INTEGER DEFAULT 50
);

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

CREATE TABLE appointments (
  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 'scheduled',
  -- 'scheduled' | 'confirmed' | 'in_progress' | 'completed' | 'cancelled' | 'no_show'
  video_room_id VARCHAR(255),
  recording_url TEXT,
  notes TEXT,
  created_at TIMESTAMPTZ DEFAULT now()
);

Слоты доступности

async function getAvailableSlots(
  specialistId: string,
  date: Date
): Promise<{ start: Date; end: Date }[]> {
  const specialist = await db.specialists.findById(specialistId);
  const dayOfWeek = date.getDay(); // 0=Sun в JS, нужно привести к 0=Mon

  // Получить рабочие часы специалиста для этого дня
  const schedule = await db.specialistSchedules.findByDayAndSpecialist(
    specialistId,
    dayOfWeek
  );
  if (!schedule) return [];

  // Занятые слоты
  const existing = await db.appointments.findBySpecialistAndDate(specialistId, date);

  const slots: { start: Date; end: Date }[] = [];
  const duration = specialist.session_duration_minutes;

  let current = setTimeOnDate(date, schedule.start_time, specialist.timezone);
  const end = setTimeOnDate(date, schedule.end_time, specialist.timezone);

  while (current < end) {
    const slotEnd = addMinutes(current, duration);

    // Проверить, не занят ли слот
    const isBusy = existing.some(
      apt => current < apt.ends_at && slotEnd > apt.starts_at
    );

    if (!isBusy && slotEnd <= end) {
      slots.push({ start: new Date(current), end: new Date(slotEnd) });
    }

    current = addMinutes(current, duration);
  }

  return slots;
}

Бронирование и подтверждение

app.post('/api/appointments', authenticate, async (req, res) => {
  const { specialistId, startsAt } = req.body;
  const specialist = await db.specialists.findById(specialistId);

  const startsAtDate = new Date(startsAt);
  const endsAt = addMinutes(startsAtDate, specialist.session_duration_minutes);

  // Проверить доступность (с пессимистичной блокировкой)
  const appointment = await db.transaction(async (trx) => {
    const conflict = await trx.query(
      `SELECT id FROM appointments
       WHERE specialist_id = $1
         AND status NOT IN ('cancelled')
         AND starts_at < $2 AND ends_at > $3
       FOR UPDATE`,
      [specialistId, endsAt, startsAtDate]
    );

    if (conflict.rows.length > 0) {
      throw new Error('Slot already taken');
    }

    return trx.query(
      `INSERT INTO appointments (specialist_id, client_id, starts_at, ends_at)
       VALUES ($1, $2, $3, $4) RETURNING *`,
      [specialistId, req.user.id, startsAtDate, endsAt]
    );
  });

  // Запланировать напоминания
  await scheduleReminders(appointment.rows[0]);

  // Уведомить специалиста
  await sendNewAppointmentNotification(specialist, appointment.rows[0], req.user);

  res.json(appointment.rows[0]);
});

Напоминания

async function scheduleReminders(appointment: Appointment) {
  const queue = new Queue('reminders', { connection });

  // За 24 часа
  await queue.add('reminder-24h', { appointmentId: appointment.id }, {
    delay: new Date(appointment.starts_at).getTime() - Date.now() - 24 * 60 * 60 * 1000,
    jobId: `reminder-24h-${appointment.id}`,
  });

  // За 1 час
  await queue.add('reminder-1h', { appointmentId: appointment.id }, {
    delay: new Date(appointment.starts_at).getTime() - Date.now() - 60 * 60 * 1000,
    jobId: `reminder-1h-${appointment.id}`,
  });

  // За 15 минут — с ссылкой на комнату
  await queue.add('reminder-15m', { appointmentId: appointment.id }, {
    delay: new Date(appointment.starts_at).getTime() - Date.now() - 15 * 60 * 1000,
    jobId: `reminder-15m-${appointment.id}`,
  });
}

Открытие видеокомнаты

app.post('/api/appointments/:id/join', authenticate, async (req, res) => {
  const appointment = await db.appointments.findById(req.params.id);

  if (!appointment) return res.status(404).json({ error: 'Not found' });

  // Проверить, что это участник консультации
  const isParticipant =
    appointment.client_id === req.user.id ||
    appointment.specialist_user_id === req.user.id;

  if (!isParticipant) return res.status(403).json({ error: 'Forbidden' });

  // Создать видеокомнату если ещё нет
  if (!appointment.video_room_id) {
    const room = await livekit.createRoom({
      name: `consultation-${appointment.id}`,
      maxParticipants: 2,
      emptyTimeout: 300,  // закрыть через 5 мин если никого
    });
    await db.appointments.update(appointment.id, { video_room_id: room.name });
    await db.appointments.update(appointment.id, { status: 'in_progress' });
  }

  const isHost = appointment.specialist_user_id === req.user.id;
  const token = await generateLiveKitToken(
    appointment.video_room_id,
    req.user.id,
    { canPublish: true, canSubscribe: true, roomAdmin: isHost }
  );

  res.json({ token, roomName: appointment.video_room_id });
});

Frontend: страница ожидания и звонок

function AppointmentRoom({ appointmentId }: { appointmentId: string }) {
  const [status, setStatus] = useState<'waiting' | 'in_call' | 'ended'>('waiting');
  const [token, setToken] = useState<string | null>(null);
  const { appointment, timeUntilStart } = useAppointment(appointmentId);

  const joinCall = async () => {
    const { token: t, roomName } = await fetch(
      `/api/appointments/${appointmentId}/join`,
      { method: 'POST' }
    ).then(r => r.json());

    setToken(t);
    setStatus('in_call');
  };

  if (status === 'waiting') {
    return (
      <div className="text-center py-16">
        <CountdownTimer seconds={timeUntilStart} />
        <p className="text-gray-600 mt-4">Специалист: {appointment.specialistName}</p>
        {timeUntilStart <= 300 && (  // показать кнопку за 5 мин
          <button onClick={joinCall} className="btn-primary mt-6">
            Войти в комнату
          </button>
        )}
      </div>
    );
  }

  if (status === 'in_call' && token) {
    return (
      <LiveKitRoom
        token={token}
        serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
        onDisconnected={() => setStatus('ended')}
      >
        <VideoConferenceUI />
      </LiveKitRoom>
    );
  }

  return <ConsultationSummary appointmentId={appointmentId} />;
}

Сроки

Полная система видеоконсультаций: расписание, бронирование, напоминания, видеозвонок, история — 2–3 недели.