From 6d40c1e98687ed2cb16ff626f83b6c05b804f8b0 Mon Sep 17 00:00:00 2001 From: chengma Date: Sun, 9 Aug 2026 10:06:42 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=BD=92=E6=A1=A3=20#41=20#43=EF=BC=8C?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=20#38=20#41=20#43=20=E9=AA=8C=E6=94=B6?= =?UTF-8?q?=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #41 归档记录了两条防信息丢失的设计(SKU 数列、待补列),以及最危险的 「部分失败」场景实测:28431952912 颜色4/尺码5 数字看着正常, 里面有 4 个 SKU 解析失败,靠整行标黄才藏不住。 #43 归档记录了实现踩到的 html/template URL 上下文转义坑: 夹在字面量 & 中间的动态内容会被整体当成一个参数值转义, ?/= 变成 %3F/%3D 让链接失效,只有真跑起来看 HTML 才发现。 也记了架构角色第二个变异第一次打偏、显示「跳过」的事—— 探针无效和代码有问题是两回事,#38 刚犯过一次。 三份都如实列了「未验证到的部分」,主要是浏览器实机交互和 列宽百分比在 table-cell 上的实际行为。 Co-Authored-By: Claude Opus 5 --- docs/task/38-admin-蝦皮excel报表导入.md | 2 +- docs/task/41-admin-蝦皮数据页商品级列表.md | 168 +++++++++++++++ .../task/43-admin-蝦皮数据页分页与状态筛选.md | 201 ++++++++++++++++++ 3 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 docs/task/41-admin-蝦皮数据页商品级列表.md create mode 100644 docs/task/43-admin-蝦皮数据页分页与状态筛选.md 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)