# Руководство по интеграции WebSocket и Webhooks в веб-админку ## Содержание 1. [Обзор](#обзор) 2. [Настройка WebSocket подключения](#настройка-websocket-подключения) 3. [Интеграция WebSocket в дашборд](#интеграция-websocket-в-дашборд) 4. [Управление Webhooks через API](#управление-webhooks-через-api) 5. [UI компоненты для Webhooks](#ui-компоненты-для-webhooks) 6. [Примеры реализации](#примеры-реализации) 7. [Обработка ошибок](#обработка-ошибок) 8. [Тестирование](#тестирование) --- ## Обзор Веб-админка может использовать два механизма для получения обновлений: 1. **WebSocket** - для real-time обновлений в интерфейсе (новые пользователи, платежи, тикеты) 2. **Webhooks** - для настройки внешних интеграций (отправка событий на внешние серверы) --- ## Настройка WebSocket подключения ### Шаг 1: Создать WebSocket менеджер Создайте утилиту для управления WebSocket подключением: ```typescript // utils/websocket.ts class WebSocketManager { private ws: WebSocket | null = null; private reconnectAttempts = 0; private maxReconnectAttempts = 5; private reconnectDelay = 1000; private listeners: Map> = new Map(); private apiToken: string; constructor(apiToken: string) { this.apiToken = apiToken; } connect(url: string): void { if (this.ws?.readyState === WebSocket.OPEN) { console.log('WebSocket already connected'); return; } const wsUrl = `${url}?token=${this.apiToken}`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket connected'); this.reconnectAttempts = 0; this.emit('connected', {}); }; this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); this.handleMessage(data); } catch (error) { console.error('Failed to parse WebSocket message:', error); } }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); this.emit('error', { error }); }; this.ws.onclose = () => { console.log('WebSocket disconnected'); this.emit('disconnected', {}); this.attemptReconnect(url); }; // Ping для keepalive каждые 30 секунд setInterval(() => { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ping' })); } }, 30000); } private handleMessage(data: any): void { if (data.type === 'pong') { return; // Игнорируем pong } if (data.type === 'connection') { this.emit('connection', data); return; } // Эмитим событие по типу this.emit(data.type, data.payload); } private attemptReconnect(url: string): void { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('Max reconnect attempts reached'); return; } this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); setTimeout(() => { console.log(`Reconnecting... (attempt ${this.reconnectAttempts})`); this.connect(url); }, delay); } on(event: string, callback: Function): void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event)!.add(callback); } off(event: string, callback: Function): void { const callbacks = this.listeners.get(event); if (callbacks) { callbacks.delete(callback); } } private emit(event: string, data: any): void { const callbacks = this.listeners.get(event); if (callbacks) { callbacks.forEach(callback => { try { callback(data); } catch (error) { console.error(`Error in event listener for ${event}:`, error); } }); } } disconnect(): void { if (this.ws) { this.ws.close(); this.ws = null; } } isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN; } } export default WebSocketManager; ``` ### Шаг 2: Инициализация в приложении ```typescript // App.tsx или main.tsx import { useEffect, useState } from 'react'; import WebSocketManager from './utils/websocket'; import { getApiToken } from './utils/auth'; function App() { const [wsManager, setWsManager] = useState(null); useEffect(() => { const token = getApiToken(); if (!token) { console.warn('No API token found, WebSocket will not connect'); return; } const manager = new WebSocketManager(token); const wsUrl = process.env.REACT_APP_WS_URL || 'ws://localhost:8080/ws'; manager.connect(wsUrl); setWsManager(manager); // Обработка событий manager.on('user.created', (payload) => { console.log('New user created:', payload); // Обновить список пользователей // Показать уведомление }); manager.on('payment.completed', (payload) => { console.log('Payment completed:', payload); // Обновить статистику // Обновить баланс пользователя }); manager.on('ticket.created', (payload) => { console.log('New ticket created:', payload); // Обновить список тикетов // Показать уведомление }); manager.on('ticket.status_changed', (payload) => { console.log('Ticket status changed:', payload); // Обновить статус тикета в списке }); manager.on('ticket.message_added', (payload) => { console.log('New message in ticket:', payload); // Обновить список сообщений в тикете // Показать уведомление о новом сообщении }); return () => { manager.disconnect(); }; }, []); return ( // Ваш компонент приложения ); } ``` --- ## Интеграция WebSocket в дашборд ### Шаг 1: Создать React Hook для WebSocket ```typescript // hooks/useWebSocket.ts import { useEffect, useState, useCallback } from 'react'; import { useWebSocketContext } from '../contexts/WebSocketContext'; export function useWebSocketEvent(eventType: string) { const { wsManager } = useWebSocketContext(); const [data, setData] = useState(null); useEffect(() => { if (!wsManager) return; const handler = (payload: T) => { setData(payload); }; wsManager.on(eventType, handler); return () => { wsManager.off(eventType, handler); }; }, [wsManager, eventType]); return data; } // Использование в компоненте function Dashboard() { const newUser = useWebSocketEvent('user.created'); const newPayment = useWebSocketEvent('payment.completed'); const newTicket = useWebSocketEvent('ticket.created'); useEffect(() => { if (newUser) { // Обновить счетчик пользователей // Показать toast уведомление } }, [newUser]); return ( // Ваш дашборд ); } ``` ### Шаг 2: Обновление счетчиков в реальном времени ```typescript // components/DashboardStats.tsx import { useState, useEffect } from 'react'; import { useWebSocketContext } from '../contexts/WebSocketContext'; import { fetchStats } from '../api/stats'; function DashboardStats() { const { wsManager } = useWebSocketContext(); const [stats, setStats] = useState({ totalUsers: 0, activeSubscriptions: 0, openTickets: 0, todayRevenue: 0, }); // Загрузка начальных данных useEffect(() => { loadStats(); }, []); // Подписка на события для обновления useEffect(() => { if (!wsManager) return; const updateOnNewUser = () => { setStats(prev => ({ ...prev, totalUsers: prev.totalUsers + 1 })); }; const updateOnPayment = (payload: any) => { setStats(prev => ({ ...prev, todayRevenue: prev.todayRevenue + (payload.amount_rubles || 0), })); }; const updateOnTicket = () => { setStats(prev => ({ ...prev, openTickets: prev.openTickets + 1 })); }; wsManager.on('user.created', updateOnNewUser); wsManager.on('payment.completed', updateOnPayment); wsManager.on('ticket.created', updateOnTicket); wsManager.on('ticket.message_added', updateOnTicketMessage); return () => { wsManager.off('user.created', updateOnNewUser); wsManager.off('payment.completed', updateOnPayment); wsManager.off('ticket.created', updateOnTicket); wsManager.off('ticket.message_added', updateOnTicketMessage); }; }, [wsManager]); const loadStats = async () => { try { const data = await fetchStats(); setStats(data); } catch (error) { console.error('Failed to load stats:', error); } }; return (
); } ``` ### Шаг 3: Уведомления о новых событиях ```typescript // components/NotificationCenter.tsx import { useState, useEffect } from 'react'; import { useWebSocketContext } from '../contexts/WebSocketContext'; import { toast } from 'react-toastify'; interface Notification { id: string; type: string; message: string; timestamp: Date; } function NotificationCenter() { const { wsManager } = useWebSocketContext(); const [notifications, setNotifications] = useState([]); useEffect(() => { if (!wsManager) return; const handleNewUser = (payload: any) => { const notification: Notification = { id: `user-${payload.user_id}`, type: 'user.created', message: `Новый пользователь: @${payload.username || payload.telegram_id}`, timestamp: new Date(), }; addNotification(notification); toast.info(notification.message); }; const handleNewPayment = (payload: any) => { const notification: Notification = { id: `payment-${payload.transaction_id}`, type: 'payment.completed', message: `Пополнение баланса: ${payload.amount_rubles} ₽`, timestamp: new Date(), }; addNotification(notification); toast.success(notification.message); }; const handleNewTicket = (payload: any) => { const notification: Notification = { id: `ticket-${payload.ticket_id}`, type: 'ticket.created', message: `Новый тикет: ${payload.title}`, timestamp: new Date(), }; addNotification(notification); toast.warning(notification.message, { onClick: () => { // Перейти к тикету window.location.href = `/tickets/${payload.ticket_id}`; }, }); }; const handleNewMessage = (payload: any) => { const notification: Notification = { id: `ticket-message-${payload.message_id}`, type: 'ticket.message_added', message: payload.is_from_admin ? `Новый ответ в тикете #${payload.ticket_id}` : `Новое сообщение от пользователя в тикете #${payload.ticket_id}`, timestamp: new Date(), }; addNotification(notification); toast.info(notification.message, { onClick: () => { // Перейти к тикету window.location.href = `/tickets/${payload.ticket_id}`; }, }); }; wsManager.on('user.created', handleNewUser); wsManager.on('payment.completed', handleNewPayment); wsManager.on('ticket.created', handleNewTicket); wsManager.on('ticket.message_added', handleNewMessage); return () => { wsManager.off('user.created', handleNewUser); wsManager.off('payment.completed', handleNewPayment); wsManager.off('ticket.created', handleNewTicket); wsManager.off('ticket.message_added', handleNewMessage); }; }, [wsManager]); const addNotification = (notification: Notification) => { setNotifications(prev => [notification, ...prev].slice(0, 50)); // Храним последние 50 }; return (
{notifications.map(notif => ( ))}
); } ``` --- ## Управление Webhooks через API ### Шаг 1: API клиент для webhooks ```typescript // api/webhooks.ts import { apiClient } from './client'; export interface Webhook { id: number; name: string; url: string; event_type: string; is_active: boolean; description?: string; created_at: string; updated_at: string; last_triggered_at?: string; failure_count: number; success_count: number; } export interface WebhookCreateRequest { name: string; url: string; event_type: string; secret?: string; description?: string; } export interface WebhookUpdateRequest { name?: string; url?: string; secret?: string; description?: string; is_active?: boolean; } export const webhooksApi = { // Список webhooks list: async (params?: { event_type?: string; is_active?: boolean; limit?: number; offset?: number; }): Promise<{ items: Webhook[]; total: number }> => { const response = await apiClient.get('/webhooks', { params }); return response.data; }, // Получить webhook get: async (id: number): Promise => { const response = await apiClient.get(`/webhooks/${id}`); return response.data; }, // Создать webhook create: async (data: WebhookCreateRequest): Promise => { const response = await apiClient.post('/webhooks', data); return response.data; }, // Обновить webhook update: async (id: number, data: WebhookUpdateRequest): Promise => { const response = await apiClient.patch(`/webhooks/${id}`, data); return response.data; }, // Удалить webhook delete: async (id: number): Promise => { await apiClient.delete(`/webhooks/${id}`); }, // Статистика getStats: async (): Promise<{ total_webhooks: number; active_webhooks: number; total_deliveries: number; successful_deliveries: number; failed_deliveries: number; success_rate: number; }> => { const response = await apiClient.get('/webhooks/stats'); return response.data; }, // История доставок getDeliveries: async ( webhookId: number, params?: { status?: string; limit?: number; offset?: number } ): Promise<{ items: any[]; total: number }> => { const response = await apiClient.get(`/webhooks/${webhookId}/deliveries`, { params }); return response.data; }, }; ``` ### Шаг 2: Список доступных типов событий ```typescript // constants/webhookEvents.ts export const WEBHOOK_EVENT_TYPES = [ { value: 'user.created', label: 'Создание пользователя', description: 'Отправляется при регистрации нового пользователя', }, { value: 'payment.completed', label: 'Завершение платежа', description: 'Отправляется при успешном пополнении баланса', }, { value: 'transaction.created', label: 'Создание транзакции', description: 'Отправляется при создании любой транзакции', }, { value: 'ticket.created', label: 'Создание тикета', description: 'Отправляется при создании нового тикета поддержки', }, { value: 'ticket.status_changed', label: 'Изменение статуса тикета', description: 'Отправляется при изменении статуса тикета', }, { value: 'ticket.message_added', label: 'Новое сообщение в тикете', description: 'Отправляется при добавлении нового сообщения в тикет (от пользователя или админа)', }, ] as const; export type WebhookEventType = typeof WEBHOOK_EVENT_TYPES[number]['value']; ``` --- ## UI компоненты для Webhooks ### Шаг 1: Форма создания/редактирования webhook ```typescript // components/WebhookForm.tsx import { useState } from 'react'; import { webhooksApi, WebhookCreateRequest, WebhookUpdateRequest } from '../api/webhooks'; import { WEBHOOK_EVENT_TYPES } from '../constants/webhookEvents'; interface WebhookFormProps { webhook?: Webhook; onSuccess: () => void; onCancel: () => void; } function WebhookForm({ webhook, onSuccess, onCancel }: WebhookFormProps) { const [formData, setFormData] = useState({ name: webhook?.name || '', url: webhook?.url || '', event_type: webhook?.event_type || '', secret: '', description: webhook?.description || '', is_active: webhook?.is_active ?? true, }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); try { if (webhook) { await webhooksApi.update(webhook.id, formData); } else { await webhooksApi.create(formData); } onSuccess(); } catch (err: any) { setError(err.response?.data?.detail || 'Ошибка при сохранении webhook'); } finally { setLoading(false); } }; return (
setFormData({ ...formData, name: e.target.value })} required />
setFormData({ ...formData, url: e.target.value })} required placeholder="https://example.com/webhook" />
{formData.event_type && ( {WEBHOOK_EVENT_TYPES.find((e) => e.value === formData.event_type)?.description} )}
setFormData({ ...formData, secret: e.target.value })} placeholder="Для подписи payload" /> Если указан, payload будет подписан с помощью HMAC-SHA256