gorm电商分页必须手写limit+offset并强制校验参数、显式排序、隔离总数查询;需用唯一性组合排序如created_at desc, id desc;总数查询须新建db实例或子查询;用scopes封装筛选逻辑;page和page_size须硬校验截断。

GORM 本身不提供 Paginate 方法,电商商品分页必须手写 Limit + Offset,并强制校验参数、显式排序、隔离总数查询——否则高并发下漏单、重复、慢查全都会发生。
电商分页必须加确定性 ORDER BY
商品列表若只写 db.Limit(20).Offset(40).Find(&goods),数据库不保证行序,尤其在后台定时上架/下架、库存扣减等写操作频繁时,同一页可能跳过某款热销商品,或重复展示刚更新的 SKU。
- 必须用带唯一性的组合排序,例如
.Order("created_at DESC, id DESC")(时间+主键)或.Order("sort_weight ASC, id ASC")(权重+主键) - 避免仅用
.Order("price ASC"):价格相同商品太多,排序结果不可预测 - MySQL 8.0+ 可考虑
.Order("id > ?", lastId)配合游标分页,但需前端配合传上一页末尾id
总数查询不能复用同一个 *gorm.DB 实例
常见错误是 db.Where("status = ?", "on_sale").Limit(20).Offset(40).Count(&total),这实际执行的是 SELECT COUNT(*) FROM ... LIMIT 20 OFFSET 40,total 永远 ≤ 20。
- 正确做法是新建上下文:
db.Session(&gorm.Session{NewDB: true}).Model(&Goods{}).Where("status = ?", "on_sale").Count(&total) - 若含
Joins或复杂关联(如查商品+最新 3 条评论),建议手写子查询:db.Raw("SELECT COUNT(*) FROM (SELECT g.id FROM goods g WHERE g.status = ?) t", "on_sale").Scan(&total) - 不要在事务中先
Count再Find:两次查询之间商品状态可能变更,总数和当前页数据对不上
用 Scopes 封装电商通用筛选逻辑
电商后台几乎每个商品列表都要处理“类目 ID、品牌、价格区间、上下架状态、关键词搜索”,硬编码这些 Where 会让控制器臃肿且难以维护。
- 定义
CategoryScope:func CategoryScope(cid uint) func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { if cid > 0 { return db.Where("category_id = ?", cid) } return db } } - 组合使用:
db.Scopes(CategoryScope(p.CatID), StatusScope(p.Status), PriceRangeScope(p.MinPrice, p.MaxPrice)).Count(&total) - 注意:
Scopes里别塞Limit/Offset—— 分页逻辑应统一收口,避免和条件逻辑耦合
page 和 page_size 必须做硬校验与截断
用户可能传 page=9999999 或 page_size=10000,直接代入 Offset 会导致 MySQL 扫描百万行后丢弃,响应超时甚至拖垮数据库。
-
page≤ 0 时强制设为 1;page_size超出配置上限(如 100)则截断:limit := util.Min(p.PageSize, 100) - 用
c.ShouldBindQuery(&p)绑定结构体,比c.Query("page")更安全——能自动拒绝非数字输入 - 别信前端传的
page_size,尤其管理后台:有人会手动改 URL 试刷,必须服务端兜底
电商分页真正的难点不在拼 SQL,而在于排序稳定性、总数准确性、参数安全性三者必须同时成立;少一个,前端分页控件就可能显示错乱,运营查不到刚上架的商品,或者 DBA 收到慢查询告警。











