角色分页必须手写LIMIT+OFFSET,显式指定ORDER避免无序导致数据重复或遗漏,排序字段需加索引且白名单校验,Page/PageSize须严格校验并兜底。

角色列表分页必须手写 Limit + Offset,GORM 没有原生 Paginate 方法,第三方插件容易掩盖边界错误,且在高偏移场景下性能陡降。
角色分页必须显式指定 Order
不加 Order 的分页结果不可靠:数据库不保证无序查询的行顺序,同一页可能重复或漏数据。角色列表通常按创建时间或名称排序,但不能依赖默认顺序。
- 必须用
Order("id ASC")或Order("created_at DESC")显式声明,字段名需与模型定义一致(如SysRole中是CreatedAt,对应数据库列是created_at) - 避免用
Order("name")这类未加索引的字段做分页主序,MySQL 可能触发 filesort,拖慢响应 - 若前端允许切换排序字段(如点击表头),需白名单校验:
if !slices.Contains([]string{"id", "role_name", "status", "updated_at"}, sortField) { sortField = "id" }
Page 和 PageSize 参数必须严格校验
用户传 page=0、page_size=-100 或 page=abc 会导致 Offset 负数 panic 或全表扫描,不能靠框架自动 fallback。
-
PageNum小于等于 0 时强制设为 1:if p.PageNum -
PageSize必须大于 0 且不超过上限(如 100):limit := int(math.Min(float64(p.PageSize), 100)) - 用
c.ShouldBindQuery(&p)绑定结构体,比c.Query("page")更安全——它会自动拒绝非整数输入并返回 error - 别把原始参数直接透传给
Offset((p.PageNum-1)*p.PageSize),先校验再计算,防止整数溢出(如page=999999999)
总数查询必须独立执行,不能复用链式 DB 实例
角色列表接口既要返回数据又要返回总条数,但 Count() 和 Find() 是两个 SQL,共用一个 *gorm.DB 实例会互相污染条件(比如 Preload 或 Where 残留)。
- 总数查法:
db.Model(&SysRole{}).Where("status = ?", 1).Count(&total)—— 注意这里用Model(&T{})重置实例 - 列表查法:
db.Where("status = ?", 1).Order("id DESC").Offset((p.PageNum-1)*limit).Limit(limit).Find(&roles) - 千万别写成:
db.Where(...).Count(&total).Offset(...).Limit(...).Find(&roles),Count()后链式调用已失效 - 如果带搜索(如
role_name LIKE ?),两个查询的Where条件必须完全一致,建议抽成函数复用
关联数据加载要避开 Preload 分页陷阱
角色列表常需显示“该角色绑定的权限数量”或“最后更新人姓名”,但若在分页查询中用 Preload("Permissions"),GORM 会先查出 10 个角色,再发 10 条 SQL 查权限,严重放大 DB 压力。
- 改用子查询或 JOIN 获取统计值:
db.Table("sys_role").Select("*, (SELECT COUNT(*) FROM role_permission_info WHERE role_id = sys_role.id) AS perm_count").Where(...) - 若需加载关联用户(如
UpdateBy字段对应用户名),优先查 ID,再用批量查(WHERE id IN (?))补全,而非Preload - 软删除字段(
DeletedAt)必须显式启用:db.Unscoped().Where(...)仅用于回收站页,正常角色列表应自动过滤已删除记录
最容易被忽略的是:当 page_size 很大(如 500)而 offset 极高(如第 10000 页)时,MySQL 执行 LIMIT 4999999,500 仍要扫描前 500 万行。这时应改用游标分页(基于 id > ?),而不是硬扛 Offset。


















