// 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 }