← กรณีการใช้งาน

ติดตามคำสั่งซื้อและการจัดส่ง

หน้าสถานะที่อัปเดตเองและพนักงานส่งที่เคลื่อนที่บนแผนที่ ส่งตำแหน่งเฉพาะตอนที่ลูกค้ากำลังดู และส่งพุชในขั้นตอนที่สำคัญ

“คำสั่งซื้อของฉันถึงไหนแล้ว?” คือคำถามที่แอปส่งของ เรียกรถ สั่งอาหาร และบริการถึงบ้านได้ยินบ่อยที่สุด หน้าสถานะที่อัปเดตเอง กับพนักงานส่งที่เคลื่อนที่บนแผนที่จริงๆ ตอบคำถามนี้ได้ก่อนที่ใครจะติดต่อฝ่ายบริการลูกค้า รูปแบบเดียวกันนี้ใช้ได้กับใบแจ้งซ่อม การจอง และทุกอย่างที่ลูกค้าต้องรอทีละขั้นตอน

สิ่งที่ผู้ใช้ได้รับ

  • สถานะคำสั่งซื้อเปลี่ยนเองจาก “กำลังเตรียม” เป็น “กำลังจัดส่ง” และ “ส่งแล้ว”
  • ตำแหน่งของพนักงานส่งขยับบนแผนที่ทุกไม่กี่วินาที
  • การแจ้งเตือนแบบพุชในขั้นตอนที่สำคัญเมื่อปิดแอปอยู่

ฟีเจอร์ที่ใช้

  • ช่องส่วนตัว (private-order-<id>) มีแต่ลูกค้าและพนักงานของคุณที่ติดตามคำสั่งซื้อได้
  • ช่องแคช (private-cache-) ลูกค้าที่เปิดหน้าจะได้สถานะและตำแหน่งล่าสุดทันที
  • Webhook channel_vacated หยุดส่งตำแหน่งเมื่อไม่มีใครดูอยู่
  • การแจ้งเตือนแบบพุช (เข้ากันได้กับ Beams) สำหรับขั้นตอนที่ลูกค้าต้องไม่พลาด

ขั้นที่ 1: ยืนยันสิทธิ์ช่องคำสั่งซื้อ

app.post('/pusher/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
  const { socket_id, channel_name } = req.body;
  const id = channel_name.match(/^private-cache-order-(\d+)$/)?.[1];
  const order = id && (await db.orders.find(id));
  if (!order || (order.customerId !== req.user.id && !req.user.isStaff)) return res.sendStatus(403);
  res.json(pusher.authorizeChannel(socket_id, channel_name));
});

ขั้นที่ 2: เผยแพร่การเปลี่ยนสถานะและตำแหน่ง

การเปลี่ยนสถานะมาจากระบบคำสั่งซื้อของคุณ ตำแหน่งมาจากแอปของพนักงานส่ง ซึ่งรายงานไปที่เซิร์ฟเวอร์ แล้วเซิร์ฟเวอร์ส่งต่อเฉพาะตอนที่มีคนดูอยู่:

async function setStatus(order, status) {
  await db.orders.update(order.id, { status });
  await pusher.trigger(`private-cache-order-${order.id}`, 'status', { status, at: Date.now() });
}

app.post('/courier/location', requireCourier, async (req, res) => {
  const order = await db.orders.activeFor(req.user.id);
  if (order && watching.has(order.id)) {
    await pusher.trigger(`private-cache-order-${order.id}`, 'location', { lat: req.body.lat, lng: req.body.lng });
  }
  res.sendStatus(204);
});

ขั้นที่ 3: แสดงในแอป

const order = pusher.subscribe(`private-cache-order-${orderId}`);
order.bind('status', ({ status }) => timeline.set(status));
order.bind('location', ({ lat, lng }) => map.moveCourier(lat, lng));

ช่องแคชเก็บเฉพาะอีเวนต์ล่าสุดเพียงอีเวนต์เดียว ไม่ว่าจะชื่ออะไร เพื่อให้ลูกค้าที่เข้ามาระหว่างสองอัปเดตได้ทั้งสถานะและตำแหน่ง ให้ใส่สถานะปัจจุบันไว้ในอีเวนต์ location ทุกครั้งด้วย

ขั้นที่ 4: ส่งเฉพาะตอนที่มีคนดู

ให้ endpoint ของ webhook รับ channel_occupied และ channel_vacated เมื่อลูกค้าปิดหน้า ช่องจะว่าง และเซิร์ฟเวอร์ของคุณจะหยุดส่งต่อตำแหน่ง:

for (const e of webhook.getEvents()) {
  const id = e.channel.match(/^private-cache-order-(\d+)$/)?.[1];
  if (!id) continue;
  if (e.name === 'channel_occupied') watching.add(id);
  if (e.name === 'channel_vacated') watching.delete(id);
}

เก็บ watching ไว้ในที่ที่เซิร์ฟเวอร์ทุกตัวใช้ร่วมกันได้ เช่น Redis หรือฐานข้อมูล

ขั้นที่ 5: พุชในขั้นตอนที่สำคัญ

await beams.publishToUsers([String(order.customerId)], {
  fcm: { notification: { title: 'Your order is on the way', body: 'Arriving in about 15 minutes.' } },
  apns: { aps: { alert: { title: 'Your order is on the way', body: 'Arriving in about 15 minutes.' } } },
  web: { notification: { title: 'Your order is on the way', deep_link: `https://your-site.example/orders/${order.id}` } },
});

ตั้งค่าอุปกรณ์ตามคู่มือการแจ้งเตือนแบบพุช

เคล็ดลับ

  • ตำแหน่งทุก 3 ถึง 5 วินาทีดูลื่นไหลเมื่อแผนที่ทำแอนิเมชันระหว่างจุด และมีต้นทุนเพียงส่วนน้อยของการส่งทุกวินาที
  • ปัดพิกัดเหลือทศนิยมห้าตำแหน่ง (ประมาณหนึ่งเมตร) ความละเอียดที่มากกว่านั้นเพิ่มแค่จำนวนไบต์
  • หยุดส่งตำแหน่งทันทีที่ส่งของเสร็จ แล้วให้แคชเก็บสถานะสุดท้ายไว้ให้คนที่มาทีหลัง