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

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, 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

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

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

Система тікетів організовує звернення користувачів: кожне звернення отримує унікальний номер, статус, пріоритет і відповідального агента. На відміну від live-чату — асинхронне спілкування з повною історією переписки. На відміну від email — прозорий SLA-контроль і звітність.

Чому email не справляється?

Звичайна пошта — це чорна скринька. Немає гарантії, що лист дійде чи потрапить потрібній людині. Тікет-система втричі скорочує час першої відповіді (згідно з нашими вимірами). Кожен запит фіксується, призначається, ескалюється при порушенні термінів.

Як спроєктувати базу даних для тікетів?

Ключова таблиця 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, обмеження прав файлової системи). Відновлення з бекапу займає години, а не дні — якщо бекапи налаштовані правильно. Регулярна підтримка запобігає таким інцидентам.

Падіння продуктивності після оновлення: 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 проектів від лендінгів до маркетплейсів.

Строки та вартість

Налаштування моніторингу та бекапів: 3–5 днів. Регулярна підтримка — ongoing контракт з фіксованим об'ємом годин на місяць або абонемент. Вартість розраховується індивідуально після аудиту. Отримайте консультацію — оцінимо ваш проект за 1–2 дні.

Порівняння: моніторинг з автоматичним алертингом vs ручна перевірка

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

Автоматичний моніторинг Better Uptime в 10 разів швидше реагує на збої, ніж ручна перевірка.