← 中文资源

简体中文版本

FastAPI 游标分页端点

在 FastAPI 实现键集游标分页:复合排序键锚定下一页、游标令牌不透明且带校验和防篡改、免总数查询直接判定续页。

阅读英文原文

示例说明

偏移分页在深页与并发写入下都不稳定,键集分页把锚点设为上一页末条记录的复合排序键:时间戳加 ID 双字段保证全序无歧义。游标令牌对客户端是不透明字符串——编码里附带的校验和让篡改与损坏在解码时立即暴露返回 400,服务端实现细节完全收敛在边界内。

技术要点

  • 复合键 (at, id) 构造无歧义全序
  • 游标带校验和防客户端篡改
  • 多取一判定法免掉总数 count 查询

代码解读

encode_cursor 把时间戳与 ID 编码后追加哈希校验和再整体 base64;decode 校验失败抛 400。list_events 按游标过滤出锚点之前的记录,切出 limit 条;len 大于 limit 说明还有下一页,用本页末条生成下一游标,否则返回 None 表示终点。

使用建议

排序键要建复合索引支撑。游标应设置时效防长期重放。前端保留上页末游标即可实现无限滚动,无需页码状态。

代码示例

import base64
import hashlib
from datetime import datetime

from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel

app = FastAPI()

EVENTS = [
    {'id': i, 'kind': 'view' if i % 2 else 'click', 'at': f'2026-10-21T10:{i:02d}:00Z'}
    for i in range(50)
]


def encode_cursor(event: dict) -> str:
    raw = f"{event['at']}|{event['id']}".encode()
    checksum = hashlib.sha256(raw).hexdigest()[:8]
    return base64.urlsafe_b64encode(raw + b'|' + checksum.encode()).decode()


def decode_cursor(token: str) -> tuple[str, int]:
    try:
        decoded = base64.urlsafe_b64decode(token.encode()).decode()
        at, oid, checksum = decoded.rsplit('|', 2)
        raw = f'{at}|{oid}'.encode()
        if hashlib.sha256(raw).hexdigest()[:8] != checksum:
            raise ValueError('checksum mismatch')
        return at, int(oid)
    except Exception as exc:
        raise HTTPException(400, 'malformed cursor') from exc


class EventPage(BaseModel):
    items: list[dict]
    next_cursor: str | None


@app.get('/events', response_model=EventPage)
def list_events(
    limit: int = Query(20, ge=1, le=100),
    cursor: str | None = None,
):
    if cursor:
        at, oid = decode_cursor(cursor)
        filtered = [e for e in EVENTS if (e['at'], e['id']) < (at, oid)]
    else:
        filtered = EVENTS

    page = filtered[:limit]
    return EventPage(
        items=page,
        next_cursor=encode_cursor(page[-1]) if len(filtered) > limit else None,
    )