vault backup: 2026-04-28 20:56:51

This commit is contained in:
2026-04-28 20:56:51 +08:00
parent 39aaa384ba
commit 0cc894a3cb
14 changed files with 1566 additions and 440 deletions
+74 -26
View File
@@ -136,25 +136,32 @@ totalPages := int(math.Ceil(float64(total) / float64(perPage)))
传统的 `LIMIT/OFFSET` 分页在深页性能急剧下降。游标分页通过「记住上一页最后一条记录的位置」来实现 O(log n) 的跳转:
```go
type User struct {
ID uint
Name string
Age int
CreatedAt time.Time
}
// 第一页:没有 cursor,直接取前 N 条
cursor := "" // 空表示从头开始
cursor := uint(0) // 用主键类型更严谨
perPage := 20
var users []User
query := db.Model(&User{}).Order("id ASC").Limit(perPage + 1) // 多取 1 条判断是否有下一页
q := db.Model(&User{}).Order("id ASC").Limit(perPage + 1) // 多取 1 条判断是否有下一页
if cursor != "" {
if cursor != 0 {
// 查找 id > cursor 的记录
query = query.Where("id > ?", cursor)
q = q.Where("id > ?", cursor)
}
err := query.Find(&users).Error
err := q.Find(&users).Error
hasNextPage := false
if len(users) > perPage {
hasNextPage = true
users = users[:perPage] // 去掉多余的「探测」记录
cursor = fmt.Sprint(users[len(users)-1].ID) // 提取新的 cursor
users = users[:perPage] // 去掉多余的「探测」记录
cursor = users[len(users)-1].ID // 提取新的 cursor
}
```
@@ -177,7 +184,7 @@ type PageResult struct {
PageSize int `json:"page_size"`
}
func Paginate[T any](db *gorm.DB, where any, order string, page, pageSize int) (*PageResult, error) {
func Paginate[T any](db *gorm.DB, where any, args ...any, order string, page, pageSize int) (*PageResult, error) {
if page < 1 {
page = 1
}
@@ -188,7 +195,7 @@ func Paginate[T any](db *gorm.DB, where any, order string, page, pageSize int) (
var total int64
q := db.Model(new(T))
if where != nil {
q = q.Where(where)
q = q.Where(where, args...)
}
if err := q.Count(&total).Error; err != nil {
return nil, err
@@ -196,7 +203,7 @@ func Paginate[T any](db *gorm.DB, where any, order string, page, pageSize int) (
var items []T
offset := (page - 1) * pageSize
q.Order(order)
q = q.Order(order)
if err := q.Limit(pageSize).Offset(offset).Find(&items).Error; err != nil {
return nil, err
}
@@ -209,10 +216,49 @@ func Paginate[T any](db *gorm.DB, where any, order string, page, pageSize int) (
}, nil
}
// 使用示例
// 无过滤条件
result, err := Paginate[User](db, nil, "created_at DESC", page, 20)
// 参数化条件查询
result, err := Paginate[User](db, "status = ?", "active", "created_at DESC", page, 20)
// 结构体条件(GORM 自动处理字段名)
result, err := Paginate[User](db, User{Status: "active"}, "created_at DESC", page, 20)
```
## HTTP Handler 中的分页
在实际项目中,分页参数通常来自 URL query string。以 Gin 框架为例:
```go
func ListUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
// 白名单校验排序字段
order := "created_at DESC"
if field := c.Query("order_by"); field != "" {
if allowedOrder[field] {
order = field + " DESC"
}
}
result, err := Paginate[User](c.MustGet("db").(*gorm.DB), nil, order, page, pageSize)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, result)
}
// GET /users?page=2&page_size=10&order_by=name
```
> [!tip] 前端推荐参数名
> - `page`:页码(从 1 开始)
> - `page_size`:每页条数
> - `order_by`:排序字段(配合后端白名单)
## DefaultPageSize 配置
GORM 提供了 `DefaultPageSize` 配置项作为全局兜底:
@@ -235,22 +281,24 @@ db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
```mermaid
flowchart TD
Start[需要分页/排序数据] --> SortQ{需要自定义排序_}
SortQ --> |否| DefaultSort["ORDER BY 主键 ASC<br/>GORM 默认排序"]
SortQ --> |是| Whitelist{"字段在白名单中?"}
Whitelist --> |是| CustomSort["db.Order(field + direction)"]
Whitelist --> |否| DefaultSort
DefaultSort --> PagesQ{要跳页还是滚动?}
Start["开始:需要分页/排序数据"] --> SortQ{"需要自定义排序?"}
SortQ -->|"否"| DefaultSort["ORDER BY 主键 ASC<br/>GORM 默认排序"]
SortQ -->|"是"| Whitelist{"字段在白名单中?"}
Whitelist -->|"是"| CustomSort["db.Order(field + direction)"]
Whitelist -->|"否"| DefaultSort
DefaultSort --> PagesQ{"要跳页还是滚动加载?"}
CustomSort --> PagesQ
PagesQ --> |跳页| OffsetPaginate["LIMIT/OFFSET 分页<br/>简单直白"]
PagesQ --> |滚动加载| CursorPaginate["游标分页<br/>性能好、防漏数据"]
OffsetPaginate --> DeepPage{是否深翻页?}
DeepPage --> |浅页 ≤ 100| OK["直接用 ✓"]
DeepPage --> |深页 > 100| CursorRecommend["推荐改用游标分页"]
PagesQ -->|"跳页"| OffsetPaginate["LIMIT / OFFSET 分页<br/>简单直白,适合浅翻页"]
PagesQ -->|"滚动加载"| CursorPaginate["游标分页<br/>性能好、防漏数据"]
OffsetPaginate --> DeepPage{"是否深翻页?"}
DeepPage -->|"浅页 ≤ 100"| OK["直接使用 ✓"]
DeepPage -->|"深页 > 100"| CursorRecommend["推荐改用游标分页"]
style Start fill:#4FC08D,color:#fff
style CursorPaginate fill:#3B82F6,color:#fff
style OK fill:#A0AEC0,color:#fff