导入真实样本后 /shopee 一次吐 3.4MB / 5195 行。但只加分页会把问题从 「5195 行糊在一起」变成「260 页里藏着 6 个」——那 6 个待补规格的商品 仍然找不到。所以分页和状态筛选一起做。 HTML 3.4MB → 16.7KB。 状态条显示全量而不是本页:「共 5195 个商品 · 第 1/260 页」。 显示「共 20 个商品」会让操作员以为总共就 20 个。筛选后显示筛选结果 总数:「待补规格:6 个商品」。 列表查询和 COUNT 共用同一套筛选条件拼装。分开写两份 WHERE,迟早 有天忘了给 COUNT 也加条件,页码算错而且没人发现(#19 踩过一次)。 page 越界兜到最后一页而不是显示空表格——空表格会让操作员以为数据没了。 总数为 0 时显示「第 1/1 页」,不出现「第 1/0 页」。 「待补规格」用 EXISTS 不用 JOIN+DISTINCT:一个商品有多个失败 SKU 时 JOIN 会出重复行,DISTINCT 又让 LIMIT/OFFSET 的行为难推理。 分页控件是 <a href> 纯 GET,浏览器前进后退和书签都正常。首末页用 <span class="disabled"> 禁用,语义上不再是链接,不只靠颜色区分。 这是全项目第一个分页页面,通用逻辑单独放 service/pagination.go 供 后面四页复用,规则写进 05 §3.2 而不是蝦皮页那一节(#34 踩过这个错)。 05 §3 的每页条数从「建议 50」改为「统一 20」并写明理由。 实现踩到 html/template 的 URL 上下文转义:夹在字面量 & 中间的动态内容 会被整体当成一个参数值转义,?/= 变成 %3F/%3D 让链接失效。改为在 Go 里 把整段 URL 拼好,模板作为单个 pipeline 输出,并加了回归测试。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
112 lines
3.3 KiB
Go
112 lines
3.3 KiB
Go
package service
|
|
|
|
import "testing"
|
|
|
|
func TestParsePage_非法值当作第1页(t *testing.T) {
|
|
cases := []string{"", "abc", "0", "-1", "-100", " "}
|
|
for _, c := range cases {
|
|
if got := ParsePage(c); got != 1 {
|
|
t.Errorf("ParsePage(%q) = %d,想要 1", c, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParsePage_合法值原样返回(t *testing.T) {
|
|
cases := map[string]int{"1": 1, "2": 2, "9999": 9999, " 3 ": 3}
|
|
for in, want := range cases {
|
|
if got := ParsePage(in); got != want {
|
|
t.Errorf("ParsePage(%q) = %d,想要 %d", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTotalPages_总数为0返回1不是0(t *testing.T) {
|
|
if got := TotalPages(0); got != 1 {
|
|
t.Errorf("TotalPages(0) = %d,想要 1(不能出现『第 1/0 页』)", got)
|
|
}
|
|
}
|
|
|
|
func TestTotalPages_按PageSize向上取整(t *testing.T) {
|
|
cases := map[int]int{
|
|
1: 1,
|
|
PageSize: 1,
|
|
PageSize + 1: 2,
|
|
PageSize * 2: 2,
|
|
5195: 260, // 工单 #43 实测样本量:5195 条 / 20 条一页 = 260 页
|
|
}
|
|
for total, want := range cases {
|
|
if got := TotalPages(total); got != want {
|
|
t.Errorf("TotalPages(%d) = %d,想要 %d", total, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClampPage_越界兜到最后一页(t *testing.T) {
|
|
if got := ClampPage(9999, 260); got != 260 {
|
|
t.Errorf("ClampPage(9999, 260) = %d,想要 260(兜到最后一页,不是空表格)", got)
|
|
}
|
|
}
|
|
|
|
func TestClampPage_小于1当作第1页(t *testing.T) {
|
|
for _, p := range []int{0, -1, -100} {
|
|
if got := ClampPage(p, 260); got != 1 {
|
|
t.Errorf("ClampPage(%d, 260) = %d,想要 1", p, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClampPage_合法范围原样返回(t *testing.T) {
|
|
if got := ClampPage(3, 260); got != 3 {
|
|
t.Errorf("ClampPage(3, 260) = %d,想要 3", got)
|
|
}
|
|
}
|
|
|
|
func TestNewPaginationView_首页禁用上一页和首页(t *testing.T) {
|
|
v := NewPaginationView(1, 260, "")
|
|
if v.HasPrev {
|
|
t.Error("第 1 页时 HasPrev 应为 false(首页/上一页要禁用)")
|
|
}
|
|
if !v.HasNext {
|
|
t.Error("第 1 页且总页数 > 1 时 HasNext 应为 true")
|
|
}
|
|
}
|
|
|
|
func TestNewPaginationView_末页禁用下一页和末页(t *testing.T) {
|
|
v := NewPaginationView(260, 260, "")
|
|
if v.HasPrev != true {
|
|
t.Error("最后一页时 HasPrev 应为 true(前面还有页)")
|
|
}
|
|
if v.HasNext {
|
|
t.Error("最后一页时 HasNext 应为 false(下一页/末页要禁用)")
|
|
}
|
|
}
|
|
|
|
func TestNewPaginationView_总数为0时首尾都禁用(t *testing.T) {
|
|
v := NewPaginationView(1, 1, "")
|
|
if v.HasPrev || v.HasNext {
|
|
t.Errorf("只有 1 页时前后都应禁用,HasPrev=%v HasNext=%v", v.HasPrev, v.HasNext)
|
|
}
|
|
}
|
|
|
|
func TestNewPaginationView_URL带上筛选条件(t *testing.T) {
|
|
v := NewPaginationView(2, 260, "status=no_link")
|
|
if v.PrevURL != "?status=no_link&page=1" {
|
|
t.Errorf("PrevURL = %q,想要 ?status=no_link&page=1", v.PrevURL)
|
|
}
|
|
if v.NextURL != "?status=no_link&page=3" {
|
|
t.Errorf("NextURL = %q,想要 ?status=no_link&page=3", v.NextURL)
|
|
}
|
|
if v.FirstURL != "?status=no_link&page=1" {
|
|
t.Errorf("FirstURL = %q,想要 ?status=no_link&page=1", v.FirstURL)
|
|
}
|
|
if v.LastURL != "?status=no_link&page=260" {
|
|
t.Errorf("LastURL = %q,想要 ?status=no_link&page=260", v.LastURL)
|
|
}
|
|
}
|
|
|
|
func TestPaginationURL_无筛选条件时只有page(t *testing.T) {
|
|
if got := PaginationURL("", 3); got != "?page=3" {
|
|
t.Errorf("PaginationURL(\"\", 3) = %q,想要 ?page=3", got)
|
|
}
|
|
}
|