GORM 的 Limit 和 Offset 不适合任务看板类多维度分页,必须改用游标分页或复合条件分页,否则在拖拽排序、实时插入、状态变更等场景下会漏项、重复、错序。

直接说结论:GORM 的 Limit 和 Offset 不适合任务看板类多维度分页,必须改用游标分页(cursor-based pagination)或复合条件分页,否则在拖拽排序、实时插入、状态变更等场景下会漏项、重复、错序。
为什么 OFFSET 分页在任务看板里必然出问题
任务看板的典型操作——比如把「进行中」列的某张卡片拖到「已完成」列顶部——会同时触发两个动作:状态更新 + 排序字段(如 sort_order 或 updated_at)变更。此时若用 OFFSET 20 LIMIT 10,第 21 条记录可能因排序变化已不在当前页,用户下拉时看到的“新一页”其实是跳过了刚插入的高优任务。
常见错误现象包括:
- 拖拽后刷新页面,卡片凭空消失(被 OFFSET 跳过)
- 多人同时操作时,同一页出现重复 ID 的卡片
-
ORDER BY updated_at DESC, id DESC配合 OFFSET,在毫秒级并发更新下无法保证稳定顺序
用 GORM 实现安全的游标分页(基于 updated_at + id)
核心思路:不记“第几页”,而记“上一页最后一条的 updated_at 和 id”,下一页查询严格大于该组合值的记录。
实操建议:
- 数据库表必须有
updated_at(带毫秒精度)和主键id两个索引字段,且联合索引(updated_at, id)效果更佳 - GORM 查询写法:
var tasks []Task db.Where("updated_at < ? OR (updated_at = ? AND id < ?)", lastCursor.UpdatedAt, lastCursor.UpdatedAt, lastCursor.ID). Order("updated_at DESC, id DESC"). Limit(10). Find(&tasks) - 前端需传入上一页末尾的
{updated_at: "2026-08-21T13:45:22.123Z", id: 105},不能只传updated_at(避免时间相同导致漏数据) - 首次请求用
Order("updated_at DESC, id DESC").Limit(10)获取首屏,返回末尾项作为初始游标
状态过滤 + 游标分页的 GORM 写法陷阱
任务看板常按列筛选(如只查 status = 'todo'),但直接加 Where("status = ?", "todo") 会导致游标失效——因为 WHERE 过滤后数据集已变,原游标值可能根本不在该子集中。
正确做法是把状态条件也纳入游标逻辑:
- 游标结构升级为
{status: "todo", updated_at: "...", id: 105} - WHERE 条件改为:
db.Where("status = ?", cursor.Status). Where("updated_at < ? OR (updated_at = ? AND id < ?)", cursor.UpdatedAt, cursor.UpdatedAt, cursor.ID) - 务必确保
(status, updated_at, id)有联合索引,否则性能急剧下降
最易被忽略的一点:游标值必须来自数据库真实返回的记录字段,不能前端拼接或客户端生成。哪怕只是把 updated_at 格式化成字符串再传回,都可能因时区、精度截断导致游标偏移。GORM 查询后,直接取 tasks[len(tasks)-1].UpdatedAt 和 .ID 作为下一页游标,别做任何转换。


















