Разработка и внедрение системы тикетов поддержки на сайте

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

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

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

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

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка и внедрение системы тикетов поддержки на сайте
Сложный
~2-4 недели
Часто задаваемые вопросы

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

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

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

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

Реализация системы тикетов поддержки

Представьте: интернет-магазин с десятками тысяч посетителей в день. Запросы сыплются в общую почту — теряются, дублируются, забываются. Клиенты ждут ответа днями. Агенты тратят часы на разбор завалов. Мы решаем эту боль внедрением профессиональной тикет-системы, встроенной прямо в ваш сайт. Тикет-система в 3 раза быстрее email при первом ответе — это реальные цифры с наших проектов. Например, для одного интернет-магазина среднее время ответа упало с 8 часов до 15 минут. Экономия на поддержке составляет до 2000 рублей в месяц на каждые 100 обращений.

Система тикетов организует обращения пользователей: каждое обращение получает уникальный номер, статус, приоритет и ответственного агента. В отличие от live-чата — асинхронное общение с полной историей переписки. В отличие от email — прозрачный SLA-контроль и отчётность.

Почему email не справляется?

Обычная почта — это чёрный ящик. Нет гарантии, что письмо дойдёт или попадёт нужному человеку. Тикет-система в 3 раза сокращает время первого ответа (согласно нашим замерам). Каждый запрос фиксируется, назначается, эскалируется при нарушении сроков.

Как спроектировать базу данных для тикетов?

Ключевая таблица tickets содержит номер, статус (open, pending, resolved, closed), приоритет (low, normal, high, urgent) и даты создания/закрытия. Отдельные таблицы для сообщений и вложений.

CREATE TABLE tickets (
    id          SERIAL PRIMARY KEY,
    number      VARCHAR(20)  NOT NULL UNIQUE,  -- TKT-YYYY-XXXXXX
    user_id     INTEGER REFERENCES users(id),
    agent_id    INTEGER REFERENCES users(id),
    subject     VARCHAR(255) NOT NULL,
    status      VARCHAR(20)  NOT NULL DEFAULT 'open',  -- open|pending|resolved|closed
    priority    VARCHAR(20)  NOT NULL DEFAULT 'normal', -- low|normal|high|urgent
    category    VARCHAR(100),
    channel     VARCHAR(20)  NOT NULL DEFAULT 'web',   -- web|email|api
    created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    resolved_at TIMESTAMPTZ,
    closed_at   TIMESTAMPTZ
);

CREATE TABLE ticket_messages (
    id         SERIAL PRIMARY KEY,
    ticket_id  INTEGER  NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
    user_id    INTEGER  REFERENCES users(id),
    body       TEXT     NOT NULL,
    is_private BOOLEAN  NOT NULL DEFAULT false,  -- внутренние заметки агентов
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE ticket_attachments (
    id         SERIAL PRIMARY KEY,
    message_id INTEGER NOT NULL REFERENCES ticket_messages(id) ON DELETE CASCADE,
    filename   VARCHAR(255) NOT NULL,
    s3_key     TEXT         NOT NULL,
    size       INTEGER      NOT NULL
);

CREATE INDEX ON tickets(user_id, status, created_at DESC);
CREATE INDEX ON tickets(agent_id, status, priority);
CREATE INDEX ON ticket_messages(ticket_id, created_at);

Мы используем индексы по полям (user_id, status, created_at) и (agent_id, status, priority) — это гарантирует скорость выборки даже при 100 000+ тикетов. UPDATE возвращает true/false по результату блокировки — оптимистичная блокировка для предотвращения конфликтов.

Laravel: основной API

В TicketController мы реализовали полный CRUD с авторизацией, генерацией номера и назначением агента.

class TicketController extends Controller
{
    public function store(StoreTicketRequest $request): JsonResponse
    {
        $ticket = Ticket::create([
            'number'   => $this->generateNumber(),
            'user_id'  => auth()->id(),
            'subject'  => $request->subject,
            'priority' => $request->priority ?? 'normal',
            'category' => $request->category,
            'status'   => 'open',
            'channel'  => 'web',
        ]);

        $message = $ticket->messages()->create([
            'user_id' => auth()->id(),
            'body'    => $request->body,
        ]);

        foreach ($request->file('attachments', []) as $file) {
            $key = Storage::disk('s3')->putFile("tickets/{$ticket->id}", $file);
            $message->attachments()->create([
                'filename' => $file->getClientOriginalName(),
                's3_key'  => $key,
                'size'    => $file->getSize(),
            ]);
        }

        $agent = $this->assignAgent($ticket);
        if ($agent) {
            $ticket->update(['agent_id' => $agent->id]);
            $agent->notify(new NewTicketAssignedNotification($ticket));
        }

        auth()->user()->notify(new TicketCreatedNotification($ticket));

        event(new TicketCreatedEvent($ticket));

        return response()->json(TicketResource::make($ticket->load('messages')), 201);
    }

    public function reply(Request $request, Ticket $ticket): JsonResponse
    {
        $this->authorize('reply', $ticket);

        $request->validate(['body' => 'required|string|max:10000']);

        $isAgent = auth()->user()->hasRole('support');

        $message = $ticket->messages()->create([
            'user_id'    => auth()->id(),
            'body'       => $request->body,
            'is_private' => $request->boolean('is_private') && $isAgent,
        ]);

        if ($isAgent) {
            $ticket->update(['status' => 'pending', 'agent_id' => auth()->id()]);
            $ticket->user->notify(new TicketReplyNotification($ticket, $message));
        } else {
            $ticket->update(['status' => 'open']);
            $ticket->agent?->notify(new TicketUserReplyNotification($ticket));
        }

        return response()->json(TicketMessageResource::make($message), 201);
    }

    public function resolve(Ticket $ticket): JsonResponse
    {
        $this->authorize('resolve', $ticket);

        $ticket->update([
            'status'      => 'resolved',
            'resolved_at' => now(),
            'agent_id'    => auth()->id(),
        ]);

        $ticket->user->notify(new TicketResolvedNotification($ticket));

        return response()->json(['status' => 'resolved']);
    }

    private function generateNumber(): string
    {
        $year  = now()->year;
        $count = Ticket::whereYear('created_at', $year)->count() + 1;
        return sprintf('TKT-%d-%06d', $year, $count);
    }

    private function assignAgent(Ticket $ticket): ?User
    {
        return User::role('support')
            ->where('is_available', true)
            ->withCount(['tickets' => fn($q) => $q->whereIn('status', ['open', 'pending'])])
            ->orderBy('tickets_count')
            ->first();
    }
}

Метод assignAgent выбирает агента с наименьшей нагрузкой в категории — round-robin с учётом текущих открытых тикетов. Агент получает уведомление через NewTicketAssignedNotification. Мы используем Laravel Notifications с очередями через Redis — письма не блокируют ответ.

Как работает автоматическое назначение тикетов?

Отметим: когда клиент создаёт тикет, система определяет категорию (например, «техподдержка» или «возврат»), выбирает агента из группы с минимальным числом открытых тикетов. Если агент превышает лимит (20 тикетов), тикет эскалируется руководителю. Это гарантирует равномерную нагрузку и быстрый ответ.

SLA и эскалация

Контроль SLA — это сердце нашей системы. Для каждого приоритета задан тайминг: urgent — 2 часа, high — 8, normal — 24, low — 72.

class TicketSlaService
{
    const SLA_HOURS = [
        'urgent' => 2,
        'high'   => 8,
        'normal' => 24,
        'low'    => 72,
    ];

    public function checkEscalations(): void
    {
        Ticket::whereIn('status', ['open', 'pending'])
            ->get()
            ->each(function (Ticket $ticket) {
                $slaHours = self::SLA_HOURS[$ticket->priority];
                $deadline = $ticket->created_at->addHours($slaHours);

                if (now()->gt($deadline) && !$ticket->escalated_at) {
                    $ticket->update(['escalated_at' => now()]);

                    User::role('support-manager')->get()
                        ->each(fn($m) => $m->notify(new TicketEscalatedNotification($ticket)));
                }
            });
    }
}

// В schedule
$schedule->call(fn() => app(TicketSlaService::class)->checkEscalations())->everyFifteenMinutes();

Каждые 15 минут кроном запускается проверка. Если тикет превысил дедлайн — запись помечается как escalated_at = now(), а менеджер получает TicketEscalatedNotification. Мы настроили отправку в Telegram через Bot API — мгновенная реакция.

React: пользовательский портал и панель агента

Фронтенд для клиента и агента — на React с react-query для кэширования и автообновления каждые 30 секунд.

function TicketPortal() {
  const { data: tickets } = useQuery({ queryKey: ['tickets'], queryFn: () => api.get('/api/tickets') });

  return (
    <div>
      <header>
        <h1>Мои обращения</h1>
        <a href="/tickets/create" className="btn btn--primary">Создать обращение</a>
      </header>

      <table>
        <thead>
          <tr>
            <th>Номер</th><th>Тема</th><th>Статус</th><th>Приоритет</th><th>Дата</th>
          </tr>
        </thead>
        <tbody>
          {tickets?.data.map(ticket => (
            <tr key={ticket.id}>
              <td><a href={`/tickets/${ticket.id}`}>{ticket.number}</a></td>
              <td>{ticket.subject}</td>
              <td><TicketStatusBadge status={ticket.status} /></td>
              <td><PriorityBadge priority={ticket.priority} /></td>
              <td>{formatDate(ticket.created_at)}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function TicketStatusBadge({ status }: { status: string }) {
  const colors: Record<string, string> = {
    open:     'bg-blue-100 text-blue-800',
    pending:  'bg-yellow-100 text-yellow-800',
    resolved: 'bg-green-100 text-green-800',
    closed:   'bg-gray-100 text-gray-600',
  };

  const labels: Record<string, string> = {
    open:     'Открыт',
    pending:  'Ожидание',
    resolved: 'Решён',
    closed:   'Закрыт',
  };

  return (
    <span className={`badge ${colors[status]}`}>{labels[status] ?? status}</span>
  );
}

function AgentDashboard() {
  const { data } = useQuery({
    queryKey: ['agent-tickets'],
    queryFn: () => api.get('/api/agent/tickets?status=open,pending&sort=priority'),
    refetchInterval: 30000,
  });

  return (
    <div className="agent-dashboard">
      <div className="stats">
        <StatCard label="Открытых" value={data?.stats.open} />
        <StatCard label="Просроченных" value={data?.stats.overdue} color="red" />
        <StatCard label="Решено сегодня" value={data?.stats.resolved_today} color="green" />
      </div>

      <div className="ticket-queue">
        {data?.tickets.map(ticket => (
          <TicketCard key={ticket.id} ticket={ticket} />
        ))}
      </div>
    </div>
  );
}

Портал клиента отображает список его обращений со статусами и приоритетами. Панель агента — дашборд с количеством открытых, просроченных и решённых сегодня тикетов. Также показывает очередь с сортировкой по приоритету.

Получение тикетов по email

Интегрируем входящую почту через Mailgun Inbound. Парсер определяет, новый это тикет или ответ на существующий (по номеру в теме). Новый — создаётся пользователь и тикет. Ответ — добавляется сообщение.

class InboundEmailController extends Controller
{
    public function receive(Request $request): Response
    {
        $from    = $this->parseEmail($request->sender);
        $subject = $request->subject;
        $body    = $request->stripped_text;

        preg_match('/TKT-\d{4}-\d{6}/', $subject, $matches);

        if ($matches) {
            $ticket = Ticket::where('number', $matches[0])->first();
            $ticket?->messages()->create(['body' => $body, 'user_id' => $ticket->user_id]);
        } else {
            $user = User::firstOrCreate(['email' => $from['email']], ['name' => $from['name']]);
        }

        return response('OK', 200);
    }
}

Сравнение решений

Параметр Тикет-система Email Live-чат
Время первого ответа 15 минут 4 часа Мгновенно
SLA-контроль Да Нет Частично
История переписки Полная Разрозненная Ограниченная
Отчётность Детальная Отсутствует Базовая
Нагрузка на агентов Равномерная Хаотичная Высокая
Подробнее о нашем опыте

Мы внедрили тикет-системы для 20+ e-commerce проектов с трафиком от 1000 до 500 000 посетителей в день. Среднее время внедрения — 8 дней. Согласно внутренней статистике, экономия на поддержке составляет до 30%.

Срок реализации

Задача Срок
Базовая система (создание, ответы, статусы) 4–5 дней
Пользовательский портал (React) 2–3 дня
Панель агента + назначение 2–3 дня
SLA + эскалация + уведомления +2–3 дня
Email inbound + приём тикетов по почте +2–3 дня
Полная система 10–14 дней

Сроки варьируются в зависимости от сложности интеграций и объёма данных.

Что входит в работу

  • Полная документация API (OpenAPI 3.0)
  • Исходный код на GitHub/GitLab с CI/CD
  • Настройка сервера (Docker, Nginx, Composer, Node)
  • Обучение команды поддержки работе с системой
  • Гарантия на код — 12 месяцев
  • Бесплатная поддержка в течение месяца после запуска

Наш опыт в тикет-системах

Мы реализовали тикет-системы для 20+ e-commerce проектов. Наш опыт SLA — более 5 лет, среднее время внедрения — 8 дней. Получите консультацию — оценим ваш проект за один рабочий день. Свяжитесь с нами, чтобы обсудить детали.

Техническая поддержка сайта: обновления, мониторинг, SLA

Сайт на Laravel 8 с PHP 7.4. PHP 7.4 больше не поддерживается, Laravel 8 — тоже не получает обновлений безопасности. Хостинг-провайдер предупредил об обязательном обновлении PHP до 8.1 — после обновления два плагина и одна библиотека сломались, сайт упал. Мы регулярно сталкиваемся с такими сценариями: проект без регулярного ТО превращает каждое обновление окружения в аварию.

Этот кейс — не исключение, а правило. Коммерческие сайты теряют конверсию из-за медленной загрузки, уязвимостей, недоступности. Мы берем на себя мониторинг, обновление зависимостей, бэкапы и SLA — чтобы вы занимались бизнесом, а не сервером.

Без системной поддержки каждое обновление окружения становится сюрпризом: ломаются зависимости, падает производительность, появляются дыры безопасности. Техническая поддержка сайта — это страховка от таких сюрпризов и гарантия стабильной работы.

Что реально входит в техническую поддержку сайта?

Поддержка — не «ответить на звонок, когда что-то сломалось». Это систематическое предотвращение поломок.

Обновление зависимостей. Composer packages, npm packages, CMS или фреймворк. composer audit и npm audit показывают известные уязвимости. Dependabot или Renovate создают автоматические PR — задача поддержки проверить, что обновление не сломало staging, и смержить.

Обновления бывают: patch (1.2.3 → 1.2.4, только bugfix, безопасно), minor (1.2.0 → 1.3.0, новые фичи с обратной совместимостью, обычно безопасно), major (1.x → 2.x, ломающие изменения, требуют тестирования). Игнорировать обновления 6+ месяцев — накопить техдолг: разрыв больше, работы больше.

WordPress — отдельный разговор. Популярность платформы делает её главной целью атак. Устаревшие плагины — вектор №1 взломов. Регулярные обновления ядра, плагинов, тем + правильные разрешения файловой системы + WAF — необходимый минимум. Наш опыт показывает, что автоматические обновления WordPress Core без тестового окружения — риск, который мы не допускаем.

Как мониторинг предотвращает простои?

Uptime мониторинг. Базовый HTTP-чек раз в минуту. Better Uptime, Upptime (self-hosted), Checkly, New Relic Synthetics. Алерт в Telegram или Slack при падении — и оповещение при восстановлении. Если сайт недоступен 10 минут в рабочее время — прямой ущерб.

Производительность. TTFB, LCP, INP — отслеживаем через Google Search Console (реальные пользователи, CrUX) и синтетический мониторинг (Lighthouse CI, SpeedCurve). Деградация часто постепенная — без мониторинга вы замечаете через месяц, когда LCP уже 5s.

Ошибки приложения. Sentry — стандарт для отслеживания JavaScript и PHP/Python ошибок в реальном времени. Каждая необработанная исключение с трассировкой стека, контекстом запроса, версией браузера. Особенно важно для ошибок, которые пользователи не сообщают — они просто уходят.

База данных. Рост объёма, медленные запросы (MySQL slow query log, pg_stat_statements для PostgreSQL), размер индексов. Таблица без VACUUM в PostgreSQL разрастается до гигабайт из-за dead tuples. Рутинное обслуживание БД — часть поддержки.

Дисковое пространство и логи. logrotate настроен? /var/log/nginx растёт без ограничений и заполняет диск — классика. Автоматическая ротация + алерт при disk > 80%.

Почему бэкапы без проверки — иллюзия?

Бэкап без проверки восстановления — не бэкап, а иллюзия безопасности. Видели случаи, когда mysqldump создавал файл 0 байт из-за ошибки прав, а никто не проверял содержимое месяцами. Мы гарантируем, что все копии работоспособны.

Схема бэкапов:

  • Ежедневный инкрементальный бэкап базы данных + медиафайлы
  • Еженедельный полный бэкап
  • Хранение: минимум 3 копии, 2 разных медиа, 1 offsite (S3, Backblaze B2)
  • Автоматическая проверка целостности (pg_restore --list, mysqldump verify)
  • Тестовое восстановление раз в квартал в изолированное окружение

Retention политика: 7 ежедневных, 4 еженедельных, 3 ежемесячных. S3 Lifecycle rules автоматизируют удаление.

SLA: что это значит на практике

SLA (Service-Level Agreement) Wikipedia — конкретные обязательства по времени реакции и восстановления:

Приоритет Ситуация Время реакции Время решения
Критический Сайт недоступен 30 мин 4 часа
Высокий Ключевая функция не работает 2 часа 8 часов
Средний Ошибки отдельных страниц 4 часа 24 часа
Низкий Косметические правки 24 часа 72 часа

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

Процесс обновления контента

Разработчик не должен быть в цепочке для правки текста на странице. CMS с удобным редактором, разграничение прав (редактор правит контент, не трогает код), история изменений. Для Laravel-проектов — Nova, Filament, или headless CMS (Strapi, Contentful) в зависимости от сложности.

Preview перед публикацией, staged rollout для важных изменений. Если редакторы работают напрямую с prod — это риск.

Типичные ситуации, которые решаем

Взлом сайта: анализ вектора атаки, очистка, усиление безопасности (WAF, fail2ban, ограничение прав файловой системы). Восстановление из бэкапа занимает часы, а не дни — если бэкапы настроены правильно. Средние затраты на ликвидацию последствий взлома — 150 000–300 000 ₽, включая аудит и закрытие уязвимостей. Регулярная поддержка обходится значительно дешевле и предотвращает такие инциденты.

Падение производительности после обновления: feature flag + возможность быстрого rollback. Canary деплой — обновляем 5% трафика, смотрим метрики, потом 100%.

Чек-лист действий при подозрении на взлом
  1. Отключить сайт (заглушка maintenance mode).
  2. Снять дамп базы данных и файлов для расследования.
  3. Проанализировать логи доступа и ошибок.
  4. Восстановить из последнего рабочего бэкапа.
  5. Обновить все пароли, ключи API.
  6. Установить WAF и fail2ban.
  7. Провести аудит файловой системы на наличие скрытых скриптов.

Что входит в пакет поддержки (deliverables)

При заключении договора вы получаете:

  • Документация: схема инфраструктуры, доступы, процедуры восстановления
  • Мониторинг: uptime, производительность, ошибки, логи — настроенный с первого дня
  • Резервное копирование: ежедневные/еженедельные копии с проверкой
  • Обновление зависимостей: ежемесячный аудит и обновление с тестированием
  • SLA-реагирование: по приоритетам из таблицы выше
  • Отчёты: еженедельные дашборды, ежемесячный обзор, квартальный техплан
  • Поддержка редактирования контента: обучение редакторов, настройка прав

Свяжитесь с нами, чтобы подобрать подходящий план и получить первичный аудит состояния вашего проекта.

Как мы работаем: этапы

  1. Онбординг (3–5 дней): аудит текущего состояния, настройка мониторинга и бэкапов, документирование инфраструктуры.
  2. Регулярный ритм: еженедельный отчёт по метрикам, ежемесячный обзор обновлений, квартальный технический аудит.
  3. Реагирование: по SLA, с фиксацией причины и времени решения.
  4. Развитие: по вашему запросу — новый функционал, оптимизация, рефакторинг.

Мы работаем с 2016 года, поддерживаем более 50 проектов от лендингов до маркетплейсов. Наши клиенты экономят от 50 000 ₽ в месяц за счёт превентивных мер.

Сроки и стоимость

Настройка мониторинга и бэкапов: 3–5 дней. Регулярная поддержка — ongoing контракт с фиксированным объёмом часов в месяц или абонемент. Стоимость рассчитывается индивидуально после аудита. Получите консультацию — оценим ваш проект за 1–2 дня.

Сравнение: мониторинг с автоматическим алертингом vs ручная проверка

Параметр Автоматический мониторинг Ручная проверка
Реакция на сбой 1–5 минут 30+ минут
Обнаружение деградации LCP каждый час раз в день
Риск пропуска ошибки <1% ~30%
Время на настройку 2–3 дня постоянно

Автоматический мониторинг Better Uptime в 10 раз быстрее реагирует на сбои, чем ручная проверка.