Files
backend_v2/internal/pkg/pagination/pagination.go
toom1996 580697f17a update
2026-09-13 14:16:37 +08:00

154 lines
4.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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