elementui分页需手动映射current-page和page-size为gorm的offset/limit,offset=(page-1)*page_size,page_size需截断上限;total必须用独立count查询;必须显式order并含唯一字段;大偏移量应改用游标分页。

ElementUI 的 <el-pagination></el-pagination> 组件和 GORM 分页不是自动对齐的,必须手动把前端传的 current-page 和 page-size 映射为 GORM 的 Offset 和 Limit,同时后端返回的 total 字段必须来自独立的 COUNT(*) 查询,否则页码会错乱或漏数据。
ElementUI 传参怎么转成 GORM 可用的 offset/limit
ElementUI 默认发的是 current-page=3&page-size=20 这类参数,Gin 中建议用结构体绑定并校验:
-
current-page对应 GORM 的页码(page),但 GORM 不认“第几页”,只认“跳过多少条”,所以要算offset = (page - 1) * page_size -
page-size直接传给Limit(),但必须截断上限(比如最大 100),避免恶意请求打爆数据库 - 空字符串、非数字、负数都会让
strconv.Atoi失败,用c.ShouldBindQuery(&p)更安全,失败时直接返回400 - 如果
page ,强制设为 <code>1;如果page_size ,也强制设为默认值(如 <code>20)
GORM 分页必须显式加 Order,否则 ElementUI 翻页会丢或重复数据
ElementUI 滚动翻页时,用户感知是“连续向下”,但 GORM 如果不加 Order,MySQL 或 PostgreSQL 返回的行序是不确定的——哪怕前后两次查同一页,也可能因索引扫描顺序不同而漏掉某条记录,或者重复出现。这不是前端 bug,是 SQL 语义问题。
- 不能只写
Order("created_at DESC"),时间字段可能重复,得补唯一字段,例如Order("created_at DESC, id DESC") - 排序字段必须有索引,否则
OFFSET越大越慢,翻到第 50 页可能超时 - 别为了“省一个字段”去掉
id,尤其在高并发插入场景下,created_at冲突概率远高于想象
后端返回 total 必须单独查,不能复用同一个 db 实例
ElementUI 的 :total 属性依赖准确总数来渲染页码栏。但如果你写成 db.Where(...).Limit(n).Offset(m).Count(&total),Count 会继承前面的 Limit 和 Offset,结果永远 ≤ n,导致页码最多只显示 1 页。
- 正确做法是另起一个干净的
*gorm.DB实例:db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total) - 如果查询带
Joins或Preload,Count容易不准,这时建议手写子查询:db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u JOIN ... ) t").Scan(&total) - 总数和列表数据不是原子操作,中间若有写入,
total和当前页数据可能轻微不一致——业务可接受就不用强一致,否则得加事务或乐观锁
大偏移量(如 page > 1000)时,ElementUI 无限滚动要切游标分页
ElementUI 的 @current-change 事件触发翻页时,如果用户手动输入极大页码(比如 5000),OFFSET 99980 在 MySQL 里实际要扫描并丢弃前 99980 行,响应时间从毫秒级飙升到秒级,还可能被 DB 主动 kill。
- 此时应放弃
current-page,改用游标模式:前端传last_id=12345,后端查WHERE id > ? ORDER BY id LIMIT 20 - ElementUI 分页组件本身不支持游标,需自定义按钮或换用
<infinite-loading></infinite-loading>类组件 - 游标字段首选主键
id(有索引、唯一、递增),次选created_at + id组合,避免时间重复导致漏数据
最常被忽略的一点:ElementUI 的 page-size 是前端可控的,但 GORM 的 Limit 是后端执行的,二者数值必须严格一致;一旦前端传了 page-size=50 却没在后端做截断,就等于把数据库暴露给了慢查询攻击。











