This commit is contained in:
toom1996
2026-09-13 14:16:37 +08:00
parent b4a0c21ce1
commit 580697f17a
2 changed files with 274 additions and 0 deletions

View File

@ -0,0 +1,153 @@
// Package pagination 提供后端分页的通用封装:查询参数解析、分页元数据计算、SQL 偏移量,
// 以及后台 HTML 模板可复用的翻页链接构造。
//
// 目标:消除各 handler 里重复的「page/size 解析 + 上/下一页计算 + 越界钳制」样板,
// 同时保证对外协议(JSON 字段名、HTML 展示文案)完全不变。
package pagination
import (
"strconv"
"github.com/gin-gonic/gin"
)
// Params 是一页请求的规范化参数(page 已钳制到 >=1;size 已钳制到 [1, maxSize])。
type Params struct {
Page int
PageSize int
}
// Parse 从 gin 查询串读取并规范化分页参数。
//
// pageKey/sizeKey 为查询参数名;sizeKey 为空表示不读取条数(固定用 defSize,不校验上限)。
// 解析规则(与既有后台列表页、history/favorite 接口保持一致):
// - page 缺失或非数字 → defPage;<1 → defPage
// - size 缺失或非数字 → defSize;<1 → defSize;>maxSize → maxSize
func Parse(c *gin.Context, pageKey, sizeKey string, defPage, defSize, maxSize int) Params {
page := atoiDefault(c.Query(pageKey), defPage)
if page < 1 {
page = defPage
}
size := defSize
if sizeKey != "" {
size = atoiDefault(c.Query(sizeKey), defSize)
if size < 1 {
size = defSize
}
if maxSize > 0 && size > maxSize {
size = maxSize
}
}
return Params{Page: page, PageSize: size}
}
// Offset 返回 GORM 翻页用的 SQL 偏移量:(page-1)*size。
func (p Params) Offset() int { return (p.Page - 1) * p.PageSize }
// Meta 是分页元数据(总页数、上/下一页、是否可翻)。
//
// NewMeta 不会修改传入的 Page(与既有 brands/runways/street/reviews 行为一致:越界页码
// 照常透传给数据库,返回空列表,仅 HasNext 据此计算)。需要越界钳制显示(ingest-jobs)
// 的调用方拿到 Meta 后调用 Clamp。
type Meta struct {
Page int
PageSize int
Total int64
TotalPages int
PrevPage int
NextPage int
HasPrev bool
HasNext bool
}
// NewMeta 计算分页元数据。
//
// HasNext 等价于「page < 总页数」,与原有 `int64(page*size) < total` 在 total>0 时一致;
// total==0 时总页数记为 1,HasNext 恒为 false(与原逻辑相同)。
func (p Params) NewMeta(total int64) Meta {
totalPages := LastPage(total, p.PageSize)
m := Meta{
Page: p.Page,
PageSize: p.PageSize,
Total: total,
TotalPages: totalPages,
}
if p.Page > 1 {
m.PrevPage = p.Page - 1
m.HasPrev = true
}
if p.Page < totalPages {
m.NextPage = p.Page + 1
m.HasNext = true
}
return m
}
// Clamp 把页码钳制到 [1, TotalPages],并重新计算 Prev/Next。
// 用于「越界页码应展示最后一页」的场景(ingest-jobs),不改变数据库查询用的原始 offset。
func (m *Meta) Clamp() {
if m.TotalPages > 0 && m.Page > m.TotalPages {
m.Page = m.TotalPages
m.HasPrev = m.Page > 1
m.HasNext = false
m.PrevPage = 0
m.NextPage = 0
if m.HasPrev {
m.PrevPage = m.Page - 1
}
}
}
// LastPage 计算总页数;无数据时返回 1(与原项目一致,避免前端出现 0 页)。
func LastPage(total int64, size int) int {
if size <= 0 {
return 1
}
last := int(total) / size
if int(total)%size > 0 {
last++
}
if last == 0 {
last = 1
}
return last
}
// Pager 是后台 HTML 模板复用的翻页视图。
//
// 由 handler 用 PageLink/PageLinkWith 生成 PrevURL/NextURL 后塞进模板数据,
// 模板只需写一行 {{template "pager" .Pager}}。
type Pager struct {
PrevURL string
NextURL string
HasPrev bool
HasNext bool
Label string // 页码信息文案(如「第 3 页 · 共 120 条」),可为空
}
// PageLink 基于当前请求构造仅替换 page 的翻页链接,保留其余查询参数。
// 用于 brands/runways/street/reviews 等(URL 路径本身已含 kind 段,无需额外拼接)。
func PageLink(c *gin.Context, page int) string {
q := c.Request.URL.Query()
q.Set("page", strconv.Itoa(page))
return c.Request.URL.Path + "?" + q.Encode()
}
// PageLinkWith 除替换 page 外,额外固定一个参数(如 ingest-jobs 的 pageSize)。
func PageLinkWith(c *gin.Context, page int, extraKey string, extraVal int) string {
q := c.Request.URL.Query()
q.Set("page", strconv.Itoa(page))
q.Set(extraKey, strconv.Itoa(extraVal))
return c.Request.URL.Path + "?" + q.Encode()
}
func atoiDefault(s string, def int) int {
if s == "" {
return def
}
n, err := strconv.Atoi(s)
if err != nil {
return def
}
return n
}