← 使用场景

订单与配送追踪

会自己更新的状态页、在地图上移动的骑手,只在顾客正在看时才发送位置,并在关键步骤发出推送。

“我的订单到哪了?”是外卖、叫车、点餐与上门服务 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 秒一个位置看起来就很顺,成本只有每秒一次的一小部分。
  • 坐标四舍五入到小数点后五位(约一米);更高的精度只会多占字节。
  • 订单一送达就停止发送位置,让缓存替晚到的人保留最终状态。