diff --git a/docs/task/38-admin-蝦皮excel报表导入.md b/docs/task/38-admin-蝦皮excel报表导入.md
index 2dd1ea5..4f073de 100644
--- a/docs/task/38-admin-蝦皮excel报表导入.md
+++ b/docs/task/38-admin-蝦皮excel报表导入.md
@@ -4,7 +4,7 @@
- 父级大工单:#14
- 所属 MVP / 版本:#15 / MVP
- 关联:#16(`pdd_products` 独立成表)、#20(迁移只追加)
-- 状态:待验收
+- 状态:验收通过
- 日期:2026-08-08
- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/38
diff --git a/docs/task/41-admin-蝦皮数据页商品级列表.md b/docs/task/41-admin-蝦皮数据页商品级列表.md
new file mode 100644
index 0000000..1f16ec8
--- /dev/null
+++ b/docs/task/41-admin-蝦皮数据页商品级列表.md
@@ -0,0 +1,168 @@
+# 41 Admin 蝦皮数据页改为商品级列表与规格弹窗
+
+- 类型:需求(界面重构)
+- 父级大工单:#14
+- 所属 MVP / 版本:#15 / MVP
+- 关联:#38(导入)、#18(弹窗机制来源)
+- 状态:验收通过
+- 日期:2026-08-08
+- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/41
+
+## 背景与目标
+
+蝦皮数据页原来是 **SKU 级一行**。问题不在行数(6092 → 5195 只少 15%,
+平均每个商品仅 1.17 个 SKU),在**语义错了**:
+
+`pdd_goods_url` / `pdd_goods_id` 在 `shopee_products` 上,是**商品级**字段。
+实测商品 `24420648774` 有 **62 个 SKU**——列表里 62 行显示的是**同一个
+PDD 链接**,将来双击任意一行编辑的也是**同一个字段**。操作员会以为
+「我改的是这一行的链接」。
+
+## 最终方案
+
+```text
+☐ │ 商品 ID │ 商品名称 │ 颜色 │ 尺码 │ SKU │ 待补 │ PDD 链接 │ 采集状态 │ 更新时间
+ 15 5 28 2
+```
+
+### 必须加「SKU 数」列
+
+`[必须]` 只显示颜色数和尺码数会**严重误导**。实测:
+
+```
+颜色数×尺码数 > 实际 SKU 数的商品:602 个(占 11.6%)
+
+goods_id 颜色数 尺码数 相乘 实际 SKU
+26886533818 15 5 75 28 ← 差 2.7 倍
+24773321934 10 6 60 14
+27739534478 12 6 72 36
+```
+
+看到「颜色 15 / 尺码 5」,正常人会以为有 75 个规格要匹配,**实际只有 28 个**。
+
+根因:蝦皮报表只含**有销售成绩的** SKU,数据本来就不全。三个数字放在一起,
+操作员才看得出「这商品在报表里只有 28 条记录,不是 75 条」。
+
+### 必须加「待补」列并整行标黄
+
+`[必须]` 原来 `parse_ok = 0` 的行显示 `—` 并整行标黄,一眼能看见要人工补。
+聚合成数量之后这个信号会丢。实测:
+
+```
+23 条解析失败的 SKU,分布在 6 个商品里
+ 4 个是「部分失败」 → 显示「颜色 3」,坏的那几个直接消失
+ 2 个是「全部失败」 → 显示「颜色 0 / 尺码 0」
+```
+
+**「部分失败」那 4 个最危险**——数字看着正常,坏数据被吃掉,永远没人去补。
+
+`[必须]` 颜色数 / 尺码数**只统计 `parse_ok = 1`** 的 SKU。
+把解析失败的空值算进 DISTINCT 会多出一个「空颜色」。
+
+### 弹窗只读,待补的行显示原文
+
+`[必须]` 待补的行必须显示 `spec_raw` 原文——不显示的话人工不知道该填什么,
+保留原文这个设计就白做了。
+
+`[必须]` 弹窗**只读**,`ShopeeSave` 仍是 501。不放一个点了没反应的保存按钮。
+
+复用 #18 的弹窗机制(`data-detail-url` / `data-detail-slot` / `data-detail-id`),
+`app.js` **未改动**。
+
+## 与建单方案的差异
+
+1. **详情路由用 `?id=` 而不是工单写的 `?goods_id=`。** `app.js` 里
+ `fetch(base + "?id=" + ...)` 的参数名是硬编码的,而「不改 `app.js`」是
+ 更高优先级的约束。与 PDD 页的 `/pdd/detail?id=` 也保持了一致。
+2. 新增 `admin/service/shopee_list_test.go`(工单预计文件表未列)。
+
+## 改了哪些
+
+- `admin/repository/shopee.go`:`ListShopeeProducts` 商品级聚合查询、
+ `GetShopeeProductByGoodsID`、`GetShopeeCollectStatus`、
+ `ListShopeeSKUsByGoodsID`;删除不再使用的 SKU 级查询。
+- `admin/service/shopee_list.go`:视图改商品级,新增弹窗用的 `ShopeeProductDetail`。
+- `admin/service/shopee_list_test.go`:新建。
+- `admin/handler/web/shopee.go`:`ShopeeList` 改写;新增只读 `ShopeeDetail`。
+- `admin/handler/web/web.go`:`GET /shopee/detail` 路由。
+- `admin/templates/shopee/list.html`:列重写、双击绑定、弹窗壳子。
+- `admin/templates/shopee/detail_modal.html`:新建。
+- `admin/static/css/app.css`:`.col-title`。
+
+## 验收结果
+
+架构角色独立复跑,用真实样本导入后逐条核对:
+
+| 检查 | 实测结果 |
+|---|---|
+| 总行数 | `共 5195 个商品` |
+| `24420648774` | **1 行**(原 62 行),颜色10 / 尺码8 / SKU**62** / 待补0 |
+| `26886533818` | 颜色**15** / 尺码**5** / SKU**28**(不是 75) |
+| **`28431952912`(部分失败)** | 颜色4 / 尺码5 / SKU24 / 待补**4**,**整行标黄** |
+| `24023581128`(全部失败) | 颜色0 / 尺码0 / SKU1 / 待补1,整行标黄 |
+| 弹窗显示原文 | `原文:面膜安全褲3條膚色,【2xl 70-85公斤可穿】` |
+| PDD 链接为空 | 显示「未填写」并标红 |
+| `app.js` / `shopee_import.go` | 均未改动 |
+| `ShopeeSave` / `Delete` / `Collect` | 仍是 501(3 处) |
+| 五个页面 | 全部 200 |
+
+`28431952912` 那条是最危险的场景——「部分失败」,颜色尺码数字看着完全正常
+(4 和 5),但里面有 4 个 SKU 解析失败。标黄之后藏不住了。
+
+## 测试
+
+架构角色亲自执行,**用 `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.049s
+ ok cmautobuy/admin/repository 0.753s
+ ok cmautobuy/admin/service 2.081s
+```
+
+### 变异测试(工单未要求,架构角色补做)
+
+| 变异 | 结果 |
+|---|---|
+| 颜色/尺码数不再过滤 `parse_ok = 1` | 4 个用例变红 |
+| 待补数恒为 0(标黄失效) | 4 个用例变红 |
+
+这两条正是本工单加进去防信息丢失的,现在有测试守着。
+
+**未验证到的部分:**
+
+- **浏览器里的真实交互**:双击开弹窗只验证了服务端 HTML 和 `/shopee/detail`
+ 的返回内容,**没在真实浏览器里点过**。
+- **1366×768 下的表格横向滚动**未做视觉走查。
+- **`max-width` 用百分比作用在 `
` 上,浏览器行为不完全一致**——
+ 部分浏览器对 `table-cell` 的 `max-width` 处理比较随意。若实机看下来
+ 宽度完全没变化,需改成固定像素(`.truncate` 用的 `max-width: 280px`
+ 那个路子是确定可行的)。
+
+用户已实机确认并验收通过。
+
+## 追加微调
+
+用户看过实际渲染后要求商品名称列「缩小到现在的 60%」,
+`max-width` 从 **30% → 18%**(提交 `ccc146c`)。
+
+理由写进 CSS 注释:蝦皮标题普遍很长(实测最长 60+ 字,全是关键词堆砌),
+不收窄的话这一列会把后面的颜色/尺码/SKU/待补几个数字挤出屏幕——
+而那几个数字才是这个页面要看的东西。完整标题靠悬停的 `title` 属性看。
+
+数值只写在 `app.css` 一处,模板注释不再重复具体百分比,
+免得下次改了数值忘了同步。
+
+## 遗留问题
+
+1. `ShopeeSave` / `ShopeeDelete` / `ShopeeCollect` 仍是 501,属后续工单。
+2. 手动新增 SKU(`05` §4.4)未做。蝦皮报表只含有销售成绩的 SKU,
+ 导入进来的数据本身就是不全的。
+3. 浏览器实机交互与列宽百分比的实际效果需人工确认。
+
+## 相关提交
+
+- `64d5e74` feat: 蝦皮数据页改为商品级列表与规格弹窗 (#41)
+- `ccc146c` style: 蝦皮商品名称列再收窄到 18% (#41)
diff --git a/docs/task/43-admin-蝦皮数据页分页与状态筛选.md b/docs/task/43-admin-蝦皮数据页分页与状态筛选.md
new file mode 100644
index 0000000..24e2138
--- /dev/null
+++ b/docs/task/43-admin-蝦皮数据页分页与状态筛选.md
@@ -0,0 +1,201 @@
+# 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 页`
+- 翻页**保留当前筛选和关键词**
+
+### 分页控件是 ``
+
+`[必须]` 纯 GET 导航,浏览器前进后退和书签都正常工作,不用 JS。
+
+`[必须]` 首末页时对应按钮用 `` 禁用——
+语义上不再是链接,且不只靠颜色区分。
+
+## 与建单方案的差异
+
+1. **分页通用逻辑单独放 `admin/service/pagination.go`**(工单预计文件表未列)。
+ 工单要求「模式要给后面四页照抄」,独立文件让后面的页面能直接 import 复用,
+ 不用从蝦皮专属文件里摘代码。
+2. **`PaginationView` 用 `FirstURL/PrevURL/NextURL/LastURL` 字段**,
+ 而不是原计划的页码字段——见下面那个坑。
+
+## 实现踩到并修掉的坑
+
+**`html/template` 的 URL 上下文转义。** 最初写成:
+
+```html
+
+```
+
+模板引擎把夹在字面量 `&` 中间的动态内容**当成单个参数值整体转义**,
+`?` 和 `=` 变成 `%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 页`) |
+| 首页时按钮 | `首页` / `上一页` |
+| 分页链接 | `?status=no_link&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 差异
+ (`` vs ``),**没在真实浏览器里点过**。
+- **1366×768 下分页控件的视觉效果**未做走查。
+- 未跑并发场景(纯只读查询,理论上不涉及并发写入)。
+
+用户已实机确认并验收通过。
+
+## 遗留问题
+
+1. 另外四个页面(PDD / 顺运宝 / 采集采购 / 客户端)**尚未接入分页**。
+ 通用逻辑已放在 `service/pagination.go`,规则已写进 `05` §3.2,
+ 后续各自开工单接入即可。
+2. 浏览器实机交互需人工确认。
+
+## 相关提交
+
+- `edfe39c` feat: 蝦皮数据页分页与状态筛选 (#43)
|