Возможности Как это работает Тарифы Документация Cookbook Партнёрам Личный кабинет
Бизнес-кейсы
📦

Отслеживание заказа

7 мин чтения Просто

Live-статус доставки: от оформления до вручения. Уведомления на каждом этапе.

E-commerce PHP JavaScript

📋 Что получится

  • Live-статус заказа без перезагрузки
  • Уведомления на каждом этапе
  • Таймлайн доставки
  • Геолокация курьера (опционально)
Любой бэкенд: Примеры на PHP, но паттерн работает с любым языком — Node.js, Python, Go и др.

🏗️ Архитектура

1
Менеджер/система меняет статус заказа
2
Сервер отправляет событие в Pushler
3
Клиент получает обновление мгновенно
4
UI обновляется: статус, таймлайн, уведомление

💻 Код сервера

Сервер обновляет статус заказа и отправляет событие клиенту. Примеры на PHP.

Установка PHP SDK

Bash
composer require pushler/php-sdk

Обновление статуса заказа

PHP
<?php
// update_order.php — обновление статуса заказа

require_once 'vendor/autoload.php';

use PushlerRu\PushlerClient;

$pushler = new PushlerClient('key_ваш_ключ', 'secret_ваш_секрет');

// Статусы заказа
$statuses = [
    'pending'    => ['text' => 'Ожидает подтверждения', 'icon' => '🕐', 'step' => 0],
    'confirmed'  => ['text' => 'Подтверждён', 'icon' => '✅', 'step' => 1],
    'preparing'  => ['text' => 'Готовится', 'icon' => '👨‍🍳', 'step' => 2],
    'ready'      => ['text' => 'Готов к выдаче', 'icon' => '📦', 'step' => 3],
    'shipped'    => ['text' => 'В доставке', 'icon' => '🚗', 'step' => 4],
    'delivered'  => ['text' => 'Доставлен', 'icon' => '🎉', 'step' => 5],
    'cancelled'  => ['text' => 'Отменён', 'icon' => '❌', 'step' => -1],
];

// Получаем данные
$orderId = (int) $_POST['order_id'];
$newStatus = $_POST['status'];
$comment = $_POST['comment'] ?? null;

// Валидация
if (!isset($statuses[$newStatus])) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid status']);
    exit;
}

// Обновляем в БД
$pdo = getDbConnection();
$stmt = $pdo->prepare('
    UPDATE orders 
    SET status = ?, updated_at = NOW() 
    WHERE id = ?
');
$stmt->execute([$newStatus, $orderId]);

// Получаем заказ
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([$orderId]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);

$statusInfo = $statuses[$newStatus];

// Отправляем событие клиенту
$pushler->trigger(
    "private-user-{$order['user_id']}",
    'order:status',
    [
        'order_id' => $orderId,
        'status' => $newStatus,
        'status_text' => $statusInfo['text'],
        'status_icon' => $statusInfo['icon'],
        'step' => $statusInfo['step'],
        'comment' => $comment,
        'updated_at' => date('c')
    ]
);

// Логируем историю
$stmt = $pdo->prepare('
    INSERT INTO order_history (order_id, status, comment, created_at) 
    VALUES (?, ?, ?, NOW())
');
$stmt->execute([$orderId, $newStatus, $comment]);

echo json_encode(['success' => true]);

Обновление геолокации курьера

PHP
<?php
// update_courier_location.php — обновление позиции курьера

require_once 'vendor/autoload.php';

use PushlerRu\PushlerClient;

$pushler = new PushlerClient('key_ваш_ключ', 'secret_ваш_секрет');

$orderId = (int) $_POST['order_id'];
$lat = (float) $_POST['lat'];
$lng = (float) $_POST['lng'];
$courierId = getCurrentCourierId();

// Получаем заказ
$pdo = getDbConnection();
$stmt = $pdo->prepare('SELECT user_id FROM orders WHERE id = ? AND courier_id = ?');
$stmt->execute([$orderId, $courierId]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$order) {
    http_response_code(403);
    exit;
}

// Отправляем геолокацию клиенту
$pushler->trigger(
    "private-user-{$order['user_id']}",
    'order:courier-location',
    [
        'order_id' => $orderId,
        'lat' => $lat,
        'lng' => $lng,
        'updated_at' => date('c')
    ]
);

echo json_encode(['success' => true]);

Авторизация канала

PHP
<?php
// auth.php

require_once 'vendor/autoload.php';

use PushlerRu\PushlerClient;

$pushler = new PushlerClient('key_ваш_ключ', 'secret_ваш_секрет');

header('Content-Type: application/json');

$channelName = $_POST['channel_name'];
$socketId = $_POST['socket_id'];
$userId = getCurrentUserId();

if (!$userId) {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

// Проверяем private-user-{userId}
if (preg_match('/^private-user-(\d+)$/', $channelName, $matches)) {
    if ((int) $matches[1] !== $userId) {
        http_response_code(403);
        echo json_encode(['error' => 'Forbidden']);
        exit;
    }
}

$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);

🌐 Код клиента

Vanilla JavaScript

JavaScript
// Отслеживание заказа
class OrderTracker {
    constructor(orderId, userId, appKey) {
        this.orderId = orderId;
        this.userId = userId;
        this.appKey = appKey;
        this.pushler = null;
        this.channel = null;
        
        this.steps = [
            { status: 'pending', label: 'Оформлен', icon: '🕐' },
            { status: 'confirmed', label: 'Подтверждён', icon: '✅' },
            { status: 'preparing', label: 'Готовится', icon: '👨‍🍳' },
            { status: 'ready', label: 'Готов', icon: '📦' },
            { status: 'shipped', label: 'В пути', icon: '🚗' },
            { status: 'delivered', label: 'Доставлен', icon: '🎉' }
        ];
        
        this.currentStep = 0;
    }

    async connect() {
        this.pushler = new PushlerClient({
            appKey: this.appKey,
            authEndpoint: '/auth.php'
        });

        return new Promise((resolve) => {
            this.pushler.on('connected', () => {
                this.channel = this.pushler.subscribe(`private-user-${this.userId}`);
                this.setupListeners();
                resolve();
            });
        });
    }

    setupListeners() {
        // Обновление статуса
        this.channel.on('order:status', (data) => {
            if (data.order_id !== this.orderId) return;
            
            this.currentStep = data.step;
            this.updateUI(data);
            this.showNotification(data);
        });

        // Геолокация курьера
        this.channel.on('order:courier-location', (data) => {
            if (data.order_id !== this.orderId) return;
            this.updateCourierMarker(data.lat, data.lng);
        });
    }

    updateUI(data) {
        // Обновляем текущий статус
        document.getElementById('current-status').textContent = data.status_text;
        document.getElementById('status-icon').textContent = data.status_icon;
        
        // Обновляем таймлайн
        this.steps.forEach((step, index) => {
            const el = document.getElementById(`step-${step.status}`);
            if (!el) return;
            
            el.classList.remove('active', 'done', 'pending');
            
            if (index < this.currentStep) {
                el.classList.add('done');
            } else if (index === this.currentStep) {
                el.classList.add('active');
            } else {
                el.classList.add('pending');
            }
        });

        // Комментарий
        if (data.comment) {
            document.getElementById('status-comment').textContent = data.comment;
        }
    }

    showNotification(data) {
        // Браузерное уведомление
        if (Notification.permission === 'granted') {
            new Notification(`Заказ #${this.orderId}`, {
                body: data.status_text,
                icon: '/icons/order.png'
            });
        }

        // Toast внутри страницы
        const toast = document.createElement('div');
        toast.className = 'toast';
        toast.innerHTML = `
            <span class="icon">${data.status_icon}</span>
            <span>${data.status_text}</span>
        `;
        document.getElementById('toasts').appendChild(toast);
        
        setTimeout(() => toast.remove(), 5000);
    }

    updateCourierMarker(lat, lng) {
        if (this.map && this.courierMarker) {
            this.courierMarker.setLatLng([lat, lng]);
        }
    }

    disconnect() {
        this.channel?.unsubscribe();
        this.pushler?.disconnect();
    }
}

// Использование
const tracker = new OrderTracker(12345, 67890, 'key_ваш_ключ');
tracker.connect();

Vue 3 компонент

Vue
<!-- OrderTracker.vue -->
<template>
  <div class="order-tracker">
    <!-- Текущий статус -->
    <div class="current-status" :class="order.status">
      <span class="icon">{{ statusInfo.icon }}</span>
      <div>
        <div class="text">{{ statusInfo.text }}</div>
        <div v-if="order.comment" class="comment">{{ order.comment }}</div>
      </div>
    </div>

    <!-- Таймлайн -->
    <div class="timeline">
      <div 
        v-for="(step, index) in steps" 
        :key="step.status"
        :class="['step', getStepClass(index)]"
      >
        <div class="dot">
          <span v-if="index < currentStep">✓</span>
          <span v-else>{{ index + 1 }}</span>
        </div>
        <div class="label">{{ step.label }}</div>
      </div>
    </div>

    <!-- Карта (если в доставке) -->
    <div v-if="order.status === 'shipped' && courierLocation" class="map-container">
      <div id="courier-map" ref="mapEl"></div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import PushlerClient from '@pushler/js';

const props = defineProps<{
  orderId: number;
  userId: number;
  initialStatus: string;
  appKey: string;
}>();

const steps = [
    { status: 'pending', label: 'Оформлен', icon: '🕐' },
    { status: 'confirmed', label: 'Подтверждён', icon: '✅' },
    { status: 'preparing', label: 'Готовится', icon: '👨‍🍳' },
    { status: 'ready', label: 'Готов', icon: '📦' },
    { status: 'shipped', label: 'В пути', icon: '🚗' },
    { status: 'delivered', label: 'Доставлен', icon: '🎉' }
];

const order = ref({
    status: props.initialStatus,
    comment: null as string | null
});

const courierLocation = ref<{ lat: number; lng: number } | null>(null);

const currentStep = computed(() => 
    steps.findIndex(s => s.status === order.value.status)
);

const statusInfo = computed(() => 
    steps.find(s => s.status === order.value.status) || steps[0]
);

const getStepClass = (index: number) => {
    if (index < currentStep.value) return 'done';
    if (index === currentStep.value) return 'active';
    return 'pending';
};

let pushler: PushlerClient | null = null;
let channel: any = null;

onMounted(() => {
    pushler = new PushlerClient({
        appKey: props.appKey,
        authEndpoint: '/auth.php'
    });

    pushler.on('connected', () => {
        channel = pushler!.subscribe(`private-user-${props.userId}`);
        
        channel.on('order:status', (data: any) => {
            if (data.order_id !== props.orderId) return;
            
            order.value.status = data.status;
            order.value.comment = data.comment;
        });

        channel.on('order:courier-location', (data: any) => {
            if (data.order_id !== props.orderId) return;
            courierLocation.value = { lat: data.lat, lng: data.lng };
        });
    });
});

onUnmounted(() => {
    channel?.unsubscribe();
    pushler?.disconnect();
});
</script>

<style scoped>
.order-tracker { padding: 24px; }

.current-status {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 20px;
    background: var(--dark-surface);
    border-radius: 12px;
    margin-bottom: 24px;
}

.current-status .icon { font-size: 2rem; }
.current-status .text { font-size: 1.25rem; font-weight: 600; }
.current-status .comment { color: var(--text-secondary); margin-top: 4px; }

.timeline {
    display: flex;
    justify-content: space-between;
    position: relative;
}

.timeline::before {
    content: '';
    position: absolute;
    top: 15px;
    left: 30px;
    right: 30px;
    height: 2px;
    background: var(--dark-border);
}

.step {
    display: flex;
    flex-direction: column;
    align-items: center;
    position: relative;
    z-index: 1;
}

.step .dot {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--dark-surface);
    border: 2px solid var(--dark-border);
    margin-bottom: 8px;
    font-size: 0.875rem;
}

.step.done .dot {
    background: #22c55e;
    border-color: #22c55e;
    color: white;
}

.step.active .dot {
    background: var(--primary-orange);
    border-color: var(--primary-orange);
    color: white;
    animation: pulse 2s infinite;
}

.step .label { font-size: 0.75rem; color: var(--text-muted); }
.step.active .label { color: var(--primary-orange); font-weight: 600; }

@keyframes pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(255, 127, 31, 0.4); }
    50% { box-shadow: 0 0 0 10px rgba(255, 127, 31, 0); }
}
</style>

⚠️ Типичные ошибки

1. Неправильный канал

PHP
// ❌ Неправильно — отправляем в канал заказа
$pushler->trigger("private-order-{$orderId}", 'order:status', $data);
// Клиенту придётся подписываться на каждый заказ!

// ✅ Правильно — отправляем в канал пользователя
$pushler->trigger("private-user-{$order['user_id']}", 'order:status', $data);
// Один канал для всех уведомлений пользователя

2. Нет проверки order_id на клиенте

JavaScript
// ❌ Неправильно — обновляем любой заказ
channel.on('order:status', (data) => {
    updateUI(data); // Если открыто несколько вкладок с разными заказами!
});

// ✅ Правильно — проверяем order_id
channel.on('order:status', (data) => {
    if (data.order_id !== currentOrderId) return;
    updateUI(data);
});

Готовы попробовать?

Создайте бесплатный аккаунт и начните интеграцию за пару минут