分页与时间区间
如何用一个不透明游标走完收货记录和集合记录,以及为什么一个接受时间区间、另一个不接受。
有两个端点会分页,而且它们有意做得不一样:
两者都返回 { "data": [...], "nextCursor": string | null },都把 limit 的上限定
在 100、默认 50,也都要求把游标原封不动地传回。本页其余部分讲的是收货记录列表;
另一个由集合页面来讲。
收货记录不可变,而且是按日期才有看头的东西,所以它接受一个区间。集合记录会原地 改变,所以在它们之上划一段区间会是一句假话——你走完这个集合,然后对账。
收货记录列表
它需要一个必填的、明确的时间区间,返回一页数据外加一个不透明的游标。
GET /v1/restaurants/{restaurantId}/deliveries
?from=1787846400000
&to=1788451200000
&limit=50
&cursor=eyJpZCI6…
| 参数 | 必填 | 规则 |
|---|---|---|
from | 是 | Unix 毫秒时间戳,写成一串数字字符串。含本身。 |
to | 是 | Unix 毫秒时间戳,写成一串数字字符串。含本身。 |
limit | 否 | 1 到 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);
}
在一次遍历的每一页上都保持 from 和 to 不变。游标编码的是你所要求的那个区间
内部的一个位置;中途更改区间不是一个受支持的请求,也不会给你想要的结果。
增量轮询
收货记录一旦记下就不可变,所以增量轮询很直接:留住你已存下的最新一条记录的
occurredAt,把它当作下一次的 from。
有两个细节值得做进去:
- 有意重叠。 把下一个窗口的起点放在你见到的最后一条记录之前一点——一小时就
足够了——并按
id去重。一台在营业期间离线的设备会在重新联网时上传,所以一条 收货记录可能在一次已经覆盖了它时间戳的轮询之后才变得可见。 - 绝不要拉宽到超过 366 天。 如果你的任务很久没跑过,就用一年长的窗口一段段走完 这个缺口,而不是一次把全部都要过来。
一页空数据并不能证明什么都没发生——见 关于完整性的说明。