← 使用情境

訂單與配送追蹤

會自己更新的狀態頁、在地圖上移動的外送員,只在顧客正在看時才傳送位置,並在重要步驟發出推播。

「我的訂單到哪了?」是外送、叫車、點餐與到府服務 App 最常被問的問題。會自己更新的狀態頁,加上地圖上真的在移動的外送員,在任何人寫信給客服之前就回答了這個問題。同樣的做法也適用於維修單、預約,以及任何顧客需要等待好幾個步驟的事情。

使用者得到什麼

  • 訂單狀態自己從「準備中」變成「配送中」再變成「已送達」。
  • 外送員在地圖上的位置每幾秒移動一次。
  • App 關著時,重要步驟會收到推播通知。

用到的功能

  • 私人頻道(private-order-<id>),只有顧客和你的員工能追蹤這張訂單。
  • 快取頻道(private-cache-),顧客一打開頁面就拿到最新的狀態與位置。
  • channel_vacated webhook,沒有人在看時就停止傳送位置。
  • 推播通知(相容 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:發布狀態變更與位置

狀態變更來自你的訂單系統。位置來自外送員 App,先回報給你的伺服器,有人在看的時候再由伺服器轉發:

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:在 App 裡顯示

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:只在有人看時傳送

讓 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 秒一個位置看起來就很順,成本只有每秒一次的一小部分。
  • 座標四捨五入到小數點後五位(約一公尺);更高的精度只會多佔位元組。
  • 訂單一送達就停止傳送位置,讓快取替晚到的人保留最終狀態。