Files
cmautobuy/docs/task/43-admin-蝦皮数据页分页与状态筛选.md
T
chengmaandClaude Opus 5 6d40c1e986 docs: 归档 #41 #43,记录 #38 #41 #43 验收通过
#41 归档记录了两条防信息丢失的设计(SKU 数列、待补列),以及最危险的
「部分失败」场景实测:28431952912 颜色4/尺码5 数字看着正常,
里面有 4 个 SKU 解析失败,靠整行标黄才藏不住。

#43 归档记录了实现踩到的 html/template URL 上下文转义坑:
夹在字面量 & 中间的动态内容会被整体当成一个参数值转义,
?/= 变成 %3F/%3D 让链接失效,只有真跑起来看 HTML 才发现。

也记了架构角色第二个变异第一次打偏、显示「跳过」的事——
探针无效和代码有问题是两回事,#38 刚犯过一次。

三份都如实列了「未验证到的部分」,主要是浏览器实机交互和
列宽百分比在 table-cell 上的实际行为。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:06:42 +08:00

202 lines
7.9 KiB
Markdown
Raw 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.
# 43 Admin 蝦皮数据页分页与状态筛选
- 类型:需求(界面)
- 父级大工单:#14
- 所属 MVP / 版本:#15 / MVP
- 关联:#38(导入)、#41(商品级列表)
- 状态:验收通过
- 日期:2026-08-08
- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/43
- `[注意]` **全项目第一个做分页的页面**,本工单定下的模式后面四个页面照抄
## 背景与目标
### ① 一次吐 3.4MB
实测导入真实样本后打开 `/shopee`:HTML **3.4 MB**、表格 **5195 行**。
### ② 但只加分页会制造新问题
```
商品总数 5195
未填 PDD 链接 5195 (100%)
有待补规格 6
```
**要找出那 6 个待补的商品,得翻 260 页。**
页面上只有「商品 ID 搜索」——得先知道 ID 才能搜。但「哪些商品需要我处理」
这个问题,恰恰是**不知道 ID 的时候才要问的**。
只做分页,情况会从「5195 行糊在一起」变成「260 页里藏着 6 个」,两种都找不到。
所以分页和状态筛选一起做。
## 最终方案
### 每页 20 条,规范一起改
`docs/admin/05` §3 原来写的是 `[建议]` 一页 50 条,
已改成 `[必须]` **五个模块统一每页 20 条**并写明理由:
20 行在 1366×768 上正好一屏不用滚动。
不改规范的话,下一个人做顺运宝页会照着 50 做,五个页面又不一致。
### 分页在数据库做,COUNT 与列表共用筛选拼装
`[必须]` `LIMIT ? OFFSET ?`,不把 5195 行查出来再在 Go 里切片——
这正是 3.4MB 的成因。
`[必须]` 总数用单独的 `COUNT(*)`,与列表查询**共用同一套筛选条件拼装函数**。
分开写两份 WHERE,迟早有一天会忘了给 COUNT 也加条件,页码算错而且没人发现。
这条 #19 已经踩过一次。
### 状态条显示全量,不是本页
`[必须]`
```text
✗ 共 20 个商品 ← 操作员以为总共就 20 个
✓ 共 5195 个商品 · 第 1/260 页
✓ 待补规格:6 个商品 · 第 1/1 页 ← 筛选后显示筛选结果总数
```
### 状态筛选四个取值
| 取值 | SQL 条件 |
|---|---|
| 全部 | — |
| 待补规格 | `EXISTS (SELECT 1 FROM shopee_skus s WHERE s.goods_id = p.goods_id AND s.parse_ok = 0)` |
| 未填 PDD 链接 | `pdd_goods_url IS NULL OR pdd_goods_url = ''` |
| 已填链接 | `pdd_goods_url IS NOT NULL AND pdd_goods_url <> ''` |
`[必须]` 「待补规格」用 `EXISTS`,**不用 `JOIN` + `DISTINCT`**——
一个商品有多个失败 SKU 时 JOIN 会出重复行,`DISTINCT` 又让
`LIMIT/OFFSET` 的行为难推理。
`[必须]` 筛选参数认不出来的一律当「全部」,不报错。
### page 越界要兜住
- `page < 1` / 非数字 → 当作 1
- `page > 总页数` → **显示最后一页,不是空表格**(空表格会让操作员以为数据没了)
- 总数为 0 → 显示 `第 1/1 页`,不出现 `第 1/0 页`
- 翻页**保留当前筛选和关键词**
### 分页控件是 `<a href>`
`[必须]` 纯 GET 导航,浏览器前进后退和书签都正常工作,不用 JS。
`[必须]` 首末页时对应按钮用 `<span class="disabled">` 禁用——
语义上不再是链接,且不只靠颜色区分。
## 与建单方案的差异
1. **分页通用逻辑单独放 `admin/service/pagination.go`**(工单预计文件表未列)。
工单要求「模式要给后面四页照抄」,独立文件让后面的页面能直接 import 复用,
不用从蝦皮专属文件里摘代码。
2. **`PaginationView` 用 `FirstURL/PrevURL/NextURL/LastURL` 字段**,
而不是原计划的页码字段——见下面那个坑。
## 实现踩到并修掉的坑
**`html/template` 的 URL 上下文转义。** 最初写成:
```html
<a href="?{{.BaseQuery}}&page={{.Next}}">
```
模板引擎把夹在字面量 `&` 中间的动态内容**当成单个参数值整体转义**,
`?` 和 `=` 变成 `%3F` / `%3D`,链接直接失效。
改为在 Go 里把整段 URL 拼好(`PaginationURL`),模板作为单个 pipeline 输出,
并加了回归测试 `TestNewPaginationView_URL带上筛选条件` /
`TestPaginationURL_无筛选条件时只有page`。
这种坑只有真跑起来看 HTML 才发现,读代码看不出来。
## 改了哪些
- `admin/repository/shopee.go`:`ShopeeFilter` / `shopeeFilterClause`(共用拼装)/
`ListShopeeProducts` 加 `LIMIT/OFFSET` / `CountShopeeProductsFiltered`。
- `admin/service/pagination.go`:新建。`PageSize=20`、`ParsePage`、`ClampPage`、
`TotalPages`、`PaginationView`、`PaginationURL`——供后面四页复用。
- `admin/service/pagination_test.go`:新建。
- `admin/service/shopee_list.go`:分页/筛选参数、状态选项、状态条文案。
- `admin/service/shopee_list_test.go`:补分页与筛选用例。
- `admin/handler/web/shopee.go`:读 `page` / `status`。
- `admin/templates/shopee/list.html`:状态筛选下拉。
- `admin/templates/partials/footer.html`:分页控件(`{{if .Pagination}}`,
对没做分页的页面无影响)。
- `admin/static/css/app.css`:`.pagination`。
- `docs/admin/05-ui-specification.md`:§3 每页条数改 20;
**新增 §3.2 分页通用规则**(写在 §3 下,不是蝦皮页那一节);§4.1、§4.5。
- `docs/admin/01-requirements.md`:§4.1 状态筛选说明。
## 验收结果
架构角色独立复跑,导入真实样本后逐条核对:
| 场景 | 实测结果 |
|---|---|
| `/shopee` | **16,693 字节** / 20 行 / `共 5195 个商品 · 第 1/260 页` |
| `?page=9999` | 13,565 字节 / **15 行有数据** / `第 260/260 页` |
| `?page=abc` | 第 1 页,不报错 |
| `?status=pending_spec` | 7,410 字节 / **6 行** / `待补规格:6 个商品 · 第 1/1 页` |
| `?status=no_link` | `未填 PDD 链接:5195 个商品 · 第 1/260 页` |
| `?status=zzz`(乱填) | 退回「全部」,20 行,不报错 |
| **空库** | `共 0 个商品 · 第 1/1 页`(不是 `第 1/0 页`) |
| 首页时按钮 | `<span class="disabled">首页` / `上一页` |
| 分页链接 | `?status=no_link&amp;page=3`,含 `%3F`/`%3D` **0 次** |
| 分页规则位置 | `05` **§3.2**(通用小节) |
| 每页条数 | `05` §3 已改为「五个模块统一每页 20 条」并写明理由 |
**HTML 大小 3.4 MB → 16.7 KB。**
## 测试
架构角色亲自执行,**用 `GOTOOLCHAIN=go1.23.0` 固定工具链**:
```
GOTOOLCHAIN=go1.23.0 go vet ./... 无输出
GOTOOLCHAIN=go1.23.0 gofmt -l . 无输出
GOTOOLCHAIN=go1.23.0 go test ./... -count=1
ok cmautobuy/admin 0.025s
ok cmautobuy/admin/repository 0.680s
ok cmautobuy/admin/service 2.018s
```
### 变异测试(工单未要求,架构角色补做)
| 变异 | 结果 |
|---|---|
| **COUNT 不再套用筛选条件**(页码算错) | 2 个用例变红 |
| **`page` 越界不再兜到最后一页** | 2 个用例变红 |
第一个是 #19 踩过的老坑,现在有测试守着了。
`[注意]` 第二个变异架构角色第一次用正则打偏了(没匹配到),
显示「跳过」。看清 `ClampPage` 的实际写法后重打才生效。
**探针无效和代码有问题是两回事**,必须先确认变异真的改变了行为——
这个教训 #38 刚犯过一次。
**未验证到的部分:**
- **浏览器里的真实交互**:分页链接点击、前进后退、书签,
只验证了生成的 HTML 结构和禁用状态的 DOM 差异
(`<span class="disabled">` vs `<a>`),**没在真实浏览器里点过**。
- **1366×768 下分页控件的视觉效果**未做走查。
- 未跑并发场景(纯只读查询,理论上不涉及并发写入)。
用户已实机确认并验收通过。
## 遗留问题
1. 另外四个页面(PDD / 顺运宝 / 采集采购 / 客户端)**尚未接入分页**。
通用逻辑已放在 `service/pagination.go`,规则已写进 `05` §3.2,
后续各自开工单接入即可。
2. 浏览器实机交互需人工确认。
## 相关提交
- `edfe39c` feat: 蝦皮数据页分页与状态筛选 (#43)