Files
cmautobuy/docs/task/43-admin-蝦皮数据页分页与状态筛选.md
T

202 lines
7.9 KiB
Markdown
Raw Normal View History

# 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)