分页与时间区间

如何用一个不透明游标走完收货记录和集合记录,以及为什么一个接受时间区间、另一个不接受。

有两个端点会分页,而且它们有意做得不一样:

列表区间排序游标绑定到
收货记录fromto必填,最长 366 天最新的在前你所要求的那个区间
集合记录没有——你走完整个集合最早的改动在前餐厅、集合以及 limit

两者都返回 { "data": [...], "nextCursor": string | null },都把 limit 的上限定 在 100、默认 50,也都要求把游标原封不动地传回。本页其余部分讲的是收货记录列表; 另一个由集合页面来讲。

收货记录不可变,而且是按日期才有看头的东西,所以它接受一个区间。集合记录会原地 改变,所以在它们之上划一段区间会是一句假话——你走完这个集合,然后对账。

收货记录列表

它需要一个必填的、明确的时间区间,返回一页数据外加一个不透明的游标。

GET /v1/restaurants/{restaurantId}/deliveries
  ?from=1787846400000
  &to=1788451200000
  &limit=50
  &cursor=eyJpZCI6…
参数必填规则
fromUnix 毫秒时间戳,写成一串数字字符串。含本身。
toUnix 毫秒时间戳,写成一串数字字符串。含本身。
limit1 到 100。默认 50。
cursor上一页的 nextCursor,一字不改地传回。

to 必须大于或等于 from,而且跨度最多 366 天。其他情况一律返回 400。这个 区间之所以必填而不是给个默认值,是因为一个有默认值的区间,正是一个集成每天夜里 悄无声息地重读十年记录的由来。

排序

最新的在前:按 occurredAt 降序,并列时再按 id 降序。于是在同一毫秒内记录的两条 收货记录也有稳定、可重复的顺序,这正是游标可靠的原因。

逐页走完

nextCursor 在最后一页是 null,其他情况下是一个字符串。原封不动地把它传回去—— 它是一个 base64url 编码的位置,不是一份供你解析的文档,它的内部结构会在不另行通知 的情况下改变。

async function* deliveries({ apiKey, restaurantId, from, to }) {
  const base = `https://api.backresto.com/v1/restaurants/${restaurantId}/deliveries`;
  let cursor = null;

  do {
    const url = new URL(base);
    url.searchParams.set('from', from);
    url.searchParams.set('to', to);
    url.searchParams.set('limit', '100');
    if (cursor !== null) {
      url.searchParams.set('cursor', cursor);
    }

    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` }
    });
    if (!response.ok) {
      throw new Error(`BackResto ${response.status}`);
    }

    const page = await response.json();
    yield* page.data;
    cursor = page.nextCursor;
  } while (cursor !== null);
}

在一次遍历的每一页上都保持 fromto 不变。游标编码的是你所要求的那个区间 内部的一个位置;中途更改区间不是一个受支持的请求,也不会给你想要的结果。

增量轮询

收货记录一旦记下就不可变,所以增量轮询很直接:留住你已存下的最新一条记录的 occurredAt,把它当作下一次的 from

有两个细节值得做进去:

  • 有意重叠。 把下一个窗口的起点放在你见到的最后一条记录之前一点——一小时就 足够了——并按 id 去重。一台在营业期间离线的设备会在重新联网时上传,所以一条 收货记录可能在一次已经覆盖了它时间戳的轮询之后才变得可见。
  • 绝不要拉宽到超过 366 天。 如果你的任务很久没跑过,就用一年长的窗口一段段走完 这个缺口,而不是一次把全部都要过来。

一页空数据并不能证明什么都没发生——见 关于完整性的说明

最后更新于 2026-09-19。