分页参数必须先校验再转换,pagenum≤0时设为1,pagesize强制限制在[1,100]区间并截断;where需链式拼接防sql注入,count须隔离会话或手写子查询,order必须显式指定以保障分页稳定性。

分页参数必须先校验再转换,不能依赖框架自动兜底
前端传 page=abc 或 page_size=0 是常态,c.ShouldBindQuery(&p) 只能拦截类型错误(比如字符串塞进 int 字段),但不会阻止 PageNum = 0 或 PageSize = -5 进入后续逻辑。一旦跳过校验,Offset((p.PageNum-1)*p.PageSize) 就可能算出负数或极大偏移,触发 MySQL 错误或查出错乱数据。
-
PageNum≤ 0 时直接设为1,别返回 400 —— 用户点错页码不是服务端该拒掉的理由 -
PageSize必须硬性限制在[1, 100]区间,超出就截断(如设为20),防恶意刷库或慢查询拖垮 DB - 空字符串、非数字值在
ShouldBindQuery阶段就会失败,此时应直接c.JSON(400, ...),不 fallback 到默认值
Where 条件必须链式拼接,禁止复用同一 *gorm.DB 实例
带搜索关键词或状态筛选的分页,容易把 Where 写成固定字符串拼接,结果 SQL 注入或类型不匹配。正确做法是从干净的 db.Model(&User{}) 开始,每加一个条件都返回新 *gorm.DB。
- 字符串模糊搜用
LIKE:写成db.Where("name LIKE ?", "%"+p.Keyword+"%"),别手写"name LIKE '%" + p.Keyword + "%" - 多个可选条件用
if p.Keyword != ""包裹,避免生成WHERE name LIKE '%%'这种无意义条件 - 别在同一个
*gorm.DB实例上反复调Where后又去Count——条件会残留,总数查不准
Count 查询必须隔离会话,或改用手写子查询
db.Where().Limit(20).Offset(40).Count(&total) 返回的 total 永远 ≤ 20,因为 Count 继承了前面的 Limit 和 Offset。这是线上最常踩的坑。
- 简单场景用
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total),靠NewDB: true隔离上下文 - 复杂
JOIN或Preload场景,Count容易出错,直接手写子查询更稳:db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u LEFT JOIN profiles p ON u.id = p.user_id WHERE ...) AS t").Scan(&total) - 如果业务允许(比如列表页不显示总页数),可只查
pageSize + 1条,用len(results) > pageSize判断是否有下一页,省掉COUNT
Order BY 不是可选项,而是分页稳定性的前提
没有显式 Order 的分页,MySQL 返回顺序不可控,翻页时数据重复或丢失是大概率事件。尤其当表有多个索引、或涉及 JOIN 时,优化器可能每次选不同执行计划。
- 必须显式指定排序字段,例如
.Order("created_at DESC")或.Order("id ASC") - 若支持前端自定义排序,需白名单校验
p.Sort值,禁止传入"id; DROP TABLE users"类注入内容 - 复合排序要写全,比如
.Order("status ASC, created_at DESC"),避免仅靠主键补位导致语义漂移
Paginate 函数就完事——Where 怎么拼、Count 怎么隔离、Order 怎么保序,三者任一环节出错,线上就可能返回错数据或慢到超时。最容易被忽略的是 Count 和主查询共用同一个 *gorm.DB 实例,这个问题在本地测不出,压测才暴露。











