移动端瀑布流分页不可用offset+limit,因数据增删会导致错位、漏数、重复加载且性能差;必须用游标分页,以前一页最后记录的created_at和id为条件查询下一批,并建立联合索引。

移动端瀑布流分页不能用 Offset + Limit,否则滑到后面会卡顿、漏数据、重复加载——这是硬伤,不是代码写得不够好。
为什么 Offset 分页在瀑布流里根本不可用
瀑布流要求“无限滚动”,用户不断下拉加载新数据,而 Offset 依赖页码计算偏移量。一旦上游数据插入/删除(比如别人刚发了一条新动态),后续所有页的 Offset 都会错位。
- 常见错误现象:
Offset(1000).Limit(20)返回第 1001–1020 条,但第 1005 条被删了,下次请求Offset(1020)就会跳过第 1021 条 - 性能崩塌点:MySQL 扫描前 10 万行再丢弃,响应从 20ms 拉到 1.8s,移动端直接超时
- 前端无法“记住位置”:用户退回再进,
page=51不等于上次看到的最后一条,体验断裂
必须用游标分页(Cursor-based Pagination)
核心是让前端传回上一页最后一条记录的排序字段值(如 created_at 和 id),后端据此查“比它更新/更大的下一批”。GORM 不内置该逻辑,得手动拼 WHERE 条件。
- 排序字段必须有联合索引,例如
INDEX idx_created_id (created_at DESC, id DESC) - 查询语句形如:
WHERE created_at - 前端首次请求不带游标,后端返回第一页 + 最后一条的
created_at和id作为 next_cursor - GORM 写法示例:
var posts []Post cursorCreatedAt := c.Query("cursor_created_at") cursorID := c.Query("cursor_id") <p>db := config.DB.Order("created_at DESC, id DESC") if cursorCreatedAt != "" && cursorID != "" { db = db.Where("created_at </p>
参数校验和边界处理不能省
移动端请求不可信,游标值可能为空、格式错、时间非法,不拦住就会 panic 或查出错乱数据。
-
cursor_created_at必须能被time.Parse(time.RFC3339)解析,失败则当首次请求处理 -
cursor_id必须是正整数,strconv.ParseUint失败就忽略游标条件 -
Limit值必须硬限制在 10–30 之间,超出按 20 截断(防恶意刷库) - 查不到数据时,仍要返回空数组 +
has_next: false,前端靠这个停掉 loading 动画
别在 GORM 分页里碰 Preload 和 COUNT
瀑布流不需要总条数,更不该为每页都跑一次 COUNT(*)。Preload 关联数据(比如文章作者、标签)也会放大问题:
-
Preload("Author")会先查 20 篇文章,再发 20 条SELECT * FROM users WHERE id IN (?, ?, ...),网络往返翻倍 - 如果用
Joins("JOIN authors..."),COUNT 子查询极易出错,且 MySQL 优化器常放弃索引 - 正确做法:分页只查主表 ID 和关键字段,前端需要详情时再按 ID 单独查(或批量查)
游标分页真正的难点不在 GORM 怎么写,而在排序字段的选择和索引覆盖是否严丝合缝——少一个 DESC 或漏建联合索引,线上就可能静默丢数据。











