צ'אט תמיכה חי מבוסס WebSocket עם תור והיסטוריה

מבקרים עוזבים כשהם לא מקבלים מענה בזמן, והעסק מאבד לידים. אנחנו מפתחים צ'אט תמיכה חי מבוסס WebSocket עם תור והיסטוריה, ומשלבים אותו באתר שלך. הצוות שלנו מספק את הפרויקט במפתח מלא—מהארכיטקטורה ועד היישום והתמיכה השוטפת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
צ'אט תמיכה חי מבוסס WebSocket עם תור והיסטוריה
מורכב
~1-2 שבועות

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1049
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

צ'אט תמיכה חי מבוסס WebSocket עם תור והיסטוריה

תארו לעצמכם חנות מקוונת עם 10,000 מבקרים יומיים שמאבדת עד 30% מהלידים כי התגובות לוקחות שעה. לקוח עוזב למתחרה שמציעה צ'אט מיידי. צ'אט מקוון באתר פותר את זה: נציג רואה את המשתמש ויכול לעזור מיד. לפי Statista, צ'אט באתר מגדיל המרות ב-20-40%. עלויות התמיכה יורדות ב-30-50% בזכות אוטומציה של שאלות טיפוסיות וקיצור זמן תגובה. אבל היישום מורכב יותר מחיבור סקריפט מוכן—הוא דורש ארכיטקטורה חזקה שמטפלת באלפי חיבורים בו-זמנית, מנהלת תור, ולעולם לא מאבדת הודעות בזמן ניתוקים. אנחנו מתכננים ופורסים מערכות כאלה למסחר אלקטרוני, SaaS ופינטק. הניסיון שלנו כולל למעלה מ-10 פרויקטים ברמת מורכבות דומה, ואנחנו מבטיחים פעילות יציבה בעומסים של עד 10K סשנים בו-זמניים.

אנחנו לוקחים על עצמנו את כל המחזור: מתכנון שרת WebSocket ועד פריסה וניטור. להלן פרטים טכניים, קוד ולוחות זמנים.

בליבה נמצא שרת WebSocket על Node.js ו-Socket.IO. הבחירה הזו מונעת מביצועים וממנגנוני התחברות מחדש מובנים.

למה WebSocket במקום Long Polling?

Long Polling יוצר עומס מיותר: כל בקשה מייצרת חבילת HTTP חדשה עם כותרות. WebSocket שומר על חיבור קבוע, מפחית תעבורה פי 10 או יותר ומוריד את זמן האחזור ל-50 אלפיות שנייה—מהיר פי 6 מ-Long Polling. לצ'אט עם קהל גדול, זה קריטי.

תכונה WebSocket Long Polling
זמן אחזור ~50 אלפיות שנייה ~300+ אלפיות שנייה
תעבורה להודעה ~100 בתים ~800 בתים (עם כותרות)
קנה מידה מתאם Redis, Nginx round-robin, מורכב יותר
התחברות מחדש אוטומטית מובנית דורשת יישום

בפועל, ההבדל מורגש ב-100+ סשנים בו-זמניים: WebSocket צורך 60% פחות משאבי CPU.

איך להבטיח חיבור WebSocket יציב בזמן ניתוקים?

Socket.IO (ראו תיעוד) מספק מנגנונים מובנים: heartbeat, timeouts, ונסיגה אקספוננציאלית. אנחנו מגדירים pingInterval ו-pingTimeout לפי העומס של הפרויקט שלכם. בזמן אובדן חיבור, הלקוח מתחבר מחדש אוטומטית, והשרת משחזר את הקשר של הצ'אט. היסטוריית ההודעות נשמרת במסד הנתונים, כך שאף הודעה לא הולכת לאיבוד.

מה קורה כשהשרת קורס?

Socket.IO שומר מצב ב-Redis. לאחר הפעלה מחדש, השרת משחזר סשנים. הודעות שלא נמסרו נשמרות במסד הנתונים ונשלחות מאוחר יותר.

דוגמה להרחבת קנה מידה ל-10K חיבורים

הרחבת WebSocket ל-10K חיבורים מושגת עם מתאם Redis ו-Nginx. כל שרת מטפל בעד 1000 חיבורים; כשחורגים מזה, נוספות מופעים חדשים. ניטור דרך Prometheus+Grafana.

ארכיטקטורה וערימת טכנולוגיות

המערכת מורכבת משלושה חלקים: שרת על Node.js + Socket.IO, ווידג'ט מבקר (React), ופאנל נציג. נתונים נשמרים ב-PostgreSQL עם אינדקסים על chat_id ו-sender_type.

צד שרת: Node.js + Socket.IO

import { Server } from 'socket.io';
import { createServer } from 'http';

const io = new Server(createServer(), {
  cors: {
    origin: process.env.FRONTEND_URL,
    credentials: true,
  },
});

// Авторизация операторов
io.use(async (socket, next) => {
  const token = socket.handshake.auth.token;
  if (token) {
    const user = await verifyToken(token);
    if (user?.role === 'operator') {
      socket.data.user = user;
      socket.data.isOperator = true;
    }
  }
  next();
});

io.on('connection', (socket) => {
  const { chatId, isOperator } = socket.data;

  // Посетитель начинает чат
  socket.on('chat:start', async (data) => {
    const chat = await Chat.create({
      visitor_name: data.name,
      visitor_email: data.email,
      page_url: data.pageUrl,
      status: 'waiting',
    });
    socket.join(`chat:${chat.id}`);
    socket.data.chatId = chat.id;

    // Уведомить всех операторов о новом чате
    io.to('operators').emit('chat:new', {
      id: chat.id,
      visitor_name: chat.visitor_name,
      page_url: chat.page_url,
      started_at: chat.created_at,
    });
    socket.emit('chat:started', { chatId: chat.id });
  });

  // Оператор принимает чат
  socket.on('chat:accept', async ({ chatId }) => {
    if (!isOperator) return;
    const chat = await Chat.findByPk(chatId);
    if (!chat || chat.status !== 'waiting') return;
    await chat.update({
      operator_id: socket.data.user.id,
      status: 'active',
      accepted_at: new Date()
    });
    socket.join(`chat:${chatId}`);

    // Уведомить посетителя
    io.to(`chat:${chatId}`).emit('chat:accepted', {
      operator: {
        name: socket.data.user.name,
        avatar: socket.data.user.avatar,
      },
    });
  });

  // Сообщение
  socket.on('chat:message', async ({ chatId, text }) => {
    const chat = await Chat.findByPk(chatId);
    if (!chat) return;
    const message = await Message.create({
      chat_id: chatId,
      sender_type: isOperator ? 'operator' : 'visitor',
      sender_id: socket.data.user?.id ?? null,
      text: sanitize(text),
    });
    io.to(`chat:${chatId}`).emit('chat:message', {
      id: message.id,
      text: message.text,
      sender_type: message.sender_type,
      sent_at: message.created_at,
    });

    // Обновить время последнего сообщения в чате
    await chat.update({ last_message_at: new Date() });
  });

  // Печатает...
  socket.on('chat:typing', ({ chatId }) => {
    socket.to(`chat:${chatId}`).emit('chat:typing', {
      sender_type: isOperator ? 'operator' : 'visitor',
    });
  });

  // Закрыть чат
  socket.on('chat:close', async ({ chatId }) => {
    await Chat.update(
      { status: 'closed', closed_at: new Date() },
      { where: { id: chatId } }
    );
    io.to(`chat:${chatId}`).emit('chat:closed');

    // Отправить транскрипт на email
    const chat = await Chat.findByPk(chatId, { include: [Message] });
    if (chat.visitor_email) {
      await emailService.sendTranscript(chat);
    }
  });

  // Операторы присоединяются к комнате для получения новых чатов
  if (isOperator) {
    socket.join('operators');
    io.to('operators').emit('operator:online', { id: socket.data.user.id });
  }

  socket.on('disconnect', async () => {
    if (isOperator) {
      io.to('operators').emit('operator:offline', { id: socket.data.user?.id });
    } else if (socket.data.chatId) {
      // Посетитель ушёл
      io.to(`chat:${socket.data.chatId}`).emit('visitor:left');
    }
  });
});

React: ווידג'ט מבקר

import { useState, useEffect, useRef } from 'react';
import { io, Socket } from 'socket.io-client';

interface Message {
  id: string;
  text: string;
  sender_type: 'visitor' | 'operator';
  sent_at: string;
}

export function ChatWidget() {
  const [isOpen, setIsOpen] = useState(false);
  const [status, setStatus] = useState<'idle' | 'starting' | 'waiting' | 'active' | 'closed'>('idle');
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [chatId, setChatId] = useState<string | null>(null);
  const [operator, setOperator] = useState<{ name: string } | null>(null);
  const socketRef = useRef<Socket | null>(null);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    socketRef.current = io(import.meta.env.VITE_CHAT_SERVER);
    const socket = socketRef.current;

    socket.on('chat:started', ({ chatId }) => {
      setChatId(chatId);
      setStatus('waiting');
    });

    socket.on('chat:accepted', ({ operator }) => {
      setOperator(operator);
      setStatus('active');
      setMessages(prev => [...prev, {
        id: 'system-accepted',
        text: `${operator.name} подключился к чату`,
        sender_type: 'operator',
        sent_at: new Date().toISOString(),
      }]);
    });

    socket.on('chat:message', (message) => {
      setMessages(prev => [...prev, message]);
    });

    socket.on('chat:closed', () => setStatus('closed'));

    return () => {
      socket.disconnect();
    };
  }, []);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  const startChat = () => {
    setStatus('starting');
    socketRef.current?.emit('chat:start', {
      name: 'Посетитель',
      pageUrl: window.location.href,
    });
  };

  const sendMessage = () => {
    if (!input.trim() || !chatId) return;
    socketRef.current?.emit('chat:message', { chatId, text: input });
    setInput('');
  };

  if (!isOpen) {
    return (
      <button className="chat-trigger" onClick={() => setIsOpen(true)} aria-label="Открыть чат">
        💬
      </button>
    );
  }

  return (
    <div className="chat-widget" role="dialog" aria-label="Чат поддержки">
      <header>
        <h2>{operator ? `Чат с ${operator.name}` : 'Чат поддержки'}</h2>
        <button onClick={() => setIsOpen(false)} aria-label="Закрыть">×</button>
      </header>
      <div className="messages" aria-live="polite">
        {status === 'idle' && (
          <div className="start-screen">
            <p>Привет! Чем можем помочь?</p>
            <button onClick={startChat}>Начать чат</button>
          </div>
        )}
        {status === 'waiting' && <p>Ищем свободного оператора...</p>}
        {messages.map(msg => (
          <div key={msg.id} className={`message message--${msg.sender_type}`}>
            <p>{msg.text}</p>
            <time>{new Date(msg.sent_at).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}</time>
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>
      {status === 'active' && (
        <footer>
          <input
            value={input}
            onChange={e => {
              setInput(e.target.value);
              socketRef.current?.emit('chat:typing', { chatId });
            }}
            onKeyDown={e => e.key === 'Enter' && !e.shiftKey && (e.preventDefault(), sendMessage())}
            placeholder="Напишите сообщение..."
          />
          <button onClick={sendMessage} disabled={!input.trim()}>Отправить</button>
        </footer>
      )}
    </div>
  );
}

פאנל נציג

function OperatorDashboard() {
  const [chats, setChats] = useState<Chat[]>([]);
  const [activeChat, setActiveChat] = useState<string | null>(null);

  // Получить очередь ожидающих чатов
  socket.on('chat:new', (chat) => {
    setChats(prev => [chat, ...prev]);
    // Звуковое уведомление
    new Audio('/notification.mp3').play().catch(() => {});
  });

  const acceptChat = (chatId: string) => {
    socket.emit('chat:accept', { chatId });
    setActiveChat(chatId);
    setChats(prev => prev.filter(c => c.id !== chatId));
  };

  return (
    <div className="operator-dashboard">
      <aside className="chat-queue">
        <h2>Ожидают ({chats.length})</h2>
        {chats.map(chat => (
          <div key={chat.id} className="queue-item">
            <span>{chat.visitor_name}</span>
            <span>{chat.page_url}</span>
            <button onClick={() => acceptChat(chat.id)}>Принять</button>
          </div>
        ))}
      </aside>
      {activeChat && <ChatWindow chatId={activeChat} socket={socket} />}
    </div>
  );
}

תהליך עבודה

  1. אנליטיקה: קביעת עומס צפוי, מספר נציגים ודרישות אינטגרציה.
  2. עיצוב: ארכיטקטורת שרת, סכמת מסד נתונים, חוזי API.
  3. יישום: פיתוח שרת, ווידג'ט ופאנל נציג במקביל.
  4. בדיקות: בדיקות עומס (עד 10K חיבורים), בדיקות סובלנות לתקלות.
  5. פריסה: הגדרת Nginx (פרוקסי WebSocket), מתאם Redis להרחבה אופקית, ניטור דרך Prometheus+Grafana.

אינטגרציה עם CRM דרך webhooks מאפשרת העברת היסטוריית שיחות. אם צריך, אנחנו מוסיפים תור (Bull/RabbitMQ) להודעות לא מקוונות ושליחת תמלילים באימייל.

טיפול בהודעות לא מקוונות

אם נציג לא מקבל צ'אט בתוך זמן קצוב, ההודעה נשמרת במסד הנתונים ותמליל נשלח לאימייל של הלקוח עם הבטחה להגיב בשעות העבודה. לעסקים בעומס גבוה, אנחנו מוסיפים הסלמה: אם התור חורג מהמגבלה, נוסף נציג שני או מופעלת אינטגרציה עם Telegram.

מה כלול בתוצאה

  • תיעוד API והוראות פריסה
  • תמונת Docker מוכנה לשרת
  • קוד מקור של הווידג'ט ופאנל הנציג
  • הדרכה לצוות התמיכה (שעה מקוונת)
  • תמיכה חינמית ל-3 חודשים לאחר ההשקה

הניסיון שלנו כולל למעלה מ-10 פרויקטים ברמת מורכבות דומה. אנחנו מציעים אחריות לקוד ל-12 חודשים. לפי Statista, צ'אט באתר מגדיל המרות ב-20-40%. יישום צ'אט תמיכה באמצעות WebSocket הוא השקעה בנאמנות לקוחות. צרו קשר כדי לדון בפרטים ולקבל גישת הדגמה לפאנל הנציג. הזמינו פיתוח של צ'אט חי למשימות שלכם ונשמח לייעץ.

לוח זמנים ליישום

משימה לוח זמנים
שרת Socket.IO + אירועים בסיסיים 2–3 ימים
ווידג'ט מבקר (React) 2–3 ימים
פאנל נציג 3–4 ימים
היסטוריית צ'אט + תמלילים באימייל +1–2 ימים
מערכת מלאה עם תור וניטור 8–12 ימים