游标分页必须用token而非裸字段值,因token可防篡改、防解析错误、携带排序方向等上下文,并规避url编码及时区歧义;裸传id或created_at会暴露数据库结构、易被恶意构造导致全表扫描。

游标分页为什么必须用 Token 而不是裸字段值
直接传 id=12345 或 created_at=2026-08-20T10:00:00Z 给前端,等于把数据库内部结构暴露出去,且极易被篡改或误用。Token 是对游标值的编码封装,核心作用是:防篡改、防解析错误、携带上下文(如排序方向、字段组合)、规避 URL 编码/时区歧义。
常见错误现象:
• 前端拼接 ?cursor=2026-08-20 10:00:00 导致 SQL 解析失败或时区错乱
• 多字段游标(如 (created_at, id))用冒号分隔却未做转义,cursor=2026-08-20T10:00:00Z:12345 在 URL 中被截断
• 游标未签名,攻击者手动构造 cursor=0001-01-01T00:00:00Z:1 触发全表扫描
- 必须用
base64.RawURLEncoding.EncodeToString()编码字节序列,不用标准 Base64(避免+和/) - 推荐结构:
struct{ Timestamp time.Time; ID uint64; Direction string }→ JSON 序列化 → 编码 - 可选加 HMAC 签名(如
hmac.Sum256([]byte(encoded + secretKey))),但多数场景只需编码+校验即可 - 解码后必须严格校验字段有效性:时间不能超前当前时间 1 小时,ID 必须 > 0,Direction 只能是
"asc"或"desc"
如何用 GORM 构造安全的游标查询条件
GORM 不支持原生行值比较(如 (created_at, id) > (?, ?))在所有方言中一致生效,MySQL 8.0+ 和 PostgreSQL 支持,SQLite 和旧版 MySQL 不支持。硬写会导致兼容性断裂。
正确做法是拆解为逻辑组合,并确保索引可用:
- 升序游标(
created_at ASC, id ASC):用WHERE created_at > ? OR (created_at = ? AND id > ?) - 降序游标(
created_at DESC, id DESC):用WHERE created_at - 必须给
created_at和id建复合索引:INDEX idx_created_id (created_at, id),否则条件无法高效走索引 - 别在游标查询里用
Preload—— 先查主键列表:db.Select("id").Where(...).Limit(pageSize + 1).Find(&ids),再用db.Where("user_id IN ?", ids).Find(&orders)
next_cursor 为空不等于“到底了”
返回 next_cursor: "" 是最常被忽略的语义陷阱。它只表示「本次查询没拿到满页数据」,但不保证下一页一定为空 —— 可能是刚插入新记录还没被索引覆盖、主从延迟、或刚好卡在边界值上。
基于官方 GMGN API 的代币分析工具。通过合约地址查询代币在 SOL/BSC/Base 链上的准确市场数据、安全检测、KOL 分析、开发者分析和 AI 智能分析(叙事/筹码/老鼠仓/机器人)。支持自动识别链。
真正可靠的判断方式是:多查 1 条(Limit(pageSize + 1)),如果结果长度 == pageSize + 1,则取前 pageSize 条返回,并用最后一条生成 next_cursor;否则返回空 next_cursor。
- 示例逻辑:
rows := make([]User, 0, pageSize+1); db.Where(...).Limit(pageSize+1).Find(&rows) - 若
len(rows) ,说明无下一页,<code>next_cursor = "" - 若
len(rows) == pageSize + 1,则rows = rows[:pageSize],并基于rows[pageSize-1]生成新游标 - 永远不要用
Count()判断是否还有下一页 —— 它和游标查询不在同一快照,结果不可比
游标 Token 的生命周期与缓存注意事项
Token 本身无状态,但其代表的“位置快照”有隐含时效性。高并发写入下,同一游标可能在不同请求中扫到不同数据集。
这不是 Bug,而是游标分页的固有特性。关键是要控制预期:
- Token 不应长期缓存(比如存在 Redis 超过 5 分钟),尤其当业务要求强一致性时
- 不要复用同一个 Token 多次请求 —— 每次响应都应生成新 Token,哪怕内容相同
- 若需支持“刷新当前页”,前端应保留原始请求参数(如首次无 cursor 的请求),而不是重放旧 Token
- 日志中记录解码后的游标值(
decoded.Timestamp,decoded.ID),便于排查“为什么某条记录跳过了”
最易被忽略的一点:游标分页天然不支持跳页(如从第 1 页直接到第 100 页),也不承诺两次请求间的数据绝对一致。接受这点,才能合理设计前端加载逻辑和用户提示文案。










