多重状态分页必须校验参数、显式排序、隔离总数查询并建合适索引,否则必出错乱或慢查询;超5000偏移须改用游标分页。

直接用 Limit + Offset 做多重状态分页检索,不加校验、不显式排序、不隔离总数查询,线上必出数据错乱或慢查询。
分页参数必须硬校验,不能信前端传的任何值
电子合同签署记录常含 pending、signed、rejected、expired 等多个状态,前端 URL 可能是 /contracts?status=signed&status=rejected&page=0&page_size=500。GORM 不会自动过滤非法值,也不拦负数或超大偏移:
-
page小于 1 时,强制设为 1(page = max(1, page)),别返回 400——用户点错“第 0 页”不是要报错 -
page_size必须截断上限,比如pageSize = min(p.PageSize, 100),防恶意刷库导致 MySQLOFFSET扫描百万行 - 多状态
status参数需白名单校验:validStatuses := map[string]bool{"pending": true, "signed": true, "rejected": true, "expired": true},不在其中的直接忽略或报错 - 用
c.ShouldBindQuery(&req)绑定结构体比c.Query("page")更安全,能统一做validate:"min=1"标签校验
WHERE 多状态 + ORDER BY 必须带索引,否则分页结果漂移
查 status IN ('signed', 'rejected') 且按 updated_at DESC 分页时,若没索引,MySQL 可能每次执行都返回不同顺序,导致同一页漏数据或重复出现——这不是 GORM 的 bug,是 SQL 语义决定的:
- 必须显式写
Order("updated_at DESC, id DESC"),避免仅靠updated_at排序(时间相同则顺序不可控) - 联合索引至少要覆盖
(status, updated_at, id),否则WHERE status IN (...) ORDER BY updated_at DESC会触发 filesort - 别在分页查询里用
Preload("Signer"):先查出 20 条合同 ID,再用db.Where("contract_id IN ?", ids).Find(&signers)批量加载,防止 N+1 或关联数据膨胀
总数 COUNT 必须独立构造,不能复用同一个 *gorm.DB 实例
常见错误是写成 db.Where("status IN ?", statuses).Offset(offset).Limit(size).Count(&total)——这返回的 total 永远 ≤ size,因为 Count() 继承了前面的 Limit 和 Offset:
- 正确做法是另起新会话:
db.Session(&gorm.Session{NewDB: true}).Model(&Contract{}).Where("status IN ?", statuses).Count(&total) - 如果 WHERE 条件复杂(比如含 JOIN 或子查询),
Count()容易不准,直接手写子查询更稳:db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM contracts WHERE status IN ?) AS t", statuses).Scan(&total) - 高并发场景下,总数和当前页数据可能不一致(比如查总数后、查列表前有新合同插入),业务上可接受就不用强一致性;若必须精确,加事务并设
Repeatable Read隔离级别
超过 5000 条偏移时,必须切游标分页
电子合同表动辄百万级,OFFSET 50000 在 MySQL 上会扫描并丢弃前 5 万行,响应从 20ms 拉到 2s+。这时 Limit/Offset 已不是 Go 层能优化的问题:
- 游标字段选
id(主键自增)或updated_at + id(防时间重复),前端首次请求不带cursor,返回next_cursor = contracts[len(contracts)-1].ID - 后续请求 SQL 改为:
WHERE id > ? AND status IN ? ORDER BY id ASC LIMIT 20,确保走PRIMARY KEY索引 - 游标分页无法跳转任意页(如“去第 87 页”),但对电子合同这类按时间/状态流式查看的场景更自然,也规避了 OFFSET 性能雪崩
多重状态分页最易被忽略的是:WHERE 条件拼接后没验证最终 SQL 是否命中索引,以及总数查询是否真的隔离——这两个点一漏,轻则分页错乱,重则 DB 被拖垮。


















