From 10b724f62a55fea8e25c6bc22e867e0a4071728b Mon Sep 17 00:00:00 2001 From: chengma Date: Sat, 8 Aug 2026 09:59:34 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=BD=92=E6=A1=A3=20#38=EF=BC=9B?= =?UTF-8?q?=E6=94=B9=E6=AD=A3=E5=85=AD=E4=BB=BD=E5=BD=92=E6=A1=A3=E9=87=8C?= =?UTF-8?q?=E6=9C=AA=E7=BB=8F=E9=AA=8C=E8=AF=81=E7=9A=84=20Go=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E8=AF=B4=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #38 归档记录了打回一次的经过(ParseSpec 两个失败分支无测试覆盖, 其中 size=="" 就是真实数据里 2 行走的分支),以及架构角色第一轮 变异打在死分支上、差点误报「测试没牙」的过程。 改正:/usr/local/go 从 2026-07-02 起一直是 1.26.5,而六份归档都写着 「架构角色亲自执行(Go 1.23.0)」——那是照项目固定版本抄的, 没有实际验证过工具链,违反 CLAUDE.md §9「说验证过必须真的跑过」。 措辞改为不宣称具体版本。已用 GOTOOLCHAIN=go1.23.0 补验,结论不变。 admin/AGENTS.md 加两条 [必须]: - 交付前至少跑一次带 GOTOOLCHAIN=go1.23.0 的验证。开发机装的可能更新, Go 会默默用它编译,测过的不是要交付的那个版本。新加依赖时尤其要跑。 - 改数据库时不要只测全新库,先列出现实中存在哪些 schema 状态再一个个验(来自 #20)。 Co-Authored-By: Claude Opus 5 --- admin/AGENTS.md | 14 ++ docs/task/16-admin-pdd-商品数据独立成表.md | 2 +- docs/task/17-admin-claim-支持领取无主任务.md | 2 +- docs/task/18-admin-pdd-商品页面.md | 2 +- docs/task/19-admin-采集采购页.md | 2 +- docs/task/20-admin-修复迁移被原地改写.md | 2 +- docs/task/24-admin-商品卡在采集中无法恢复.md | 2 +- docs/task/38-admin-蝦皮excel报表导入.md | 243 +++++++++++++++++++ 8 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 docs/task/38-admin-蝦皮excel报表导入.md diff --git a/admin/AGENTS.md b/admin/AGENTS.md index 598cc5c..051dcd0 100644 --- a/admin/AGENTS.md +++ b/admin/AGENTS.md @@ -119,8 +119,22 @@ go test ./... go run . ``` +`[必须]` **交付前至少跑一次带 `GOTOOLCHAIN=go1.23.0` 的验证**: + +```powershell +$env:GOTOOLCHAIN="go1.23.0"; go build ./...; go test ./... -count=1 +Remove-Item Env:GOTOOLCHAIN +``` + +本项目固定 Go 1.23.0(见 [00 上手指南](../docs/admin/00-getting-started.md)), +但开发机上装的可能是更新的版本,Go 会默默用它编译,**测过的不是要交付的那个版本**。 +本项目已经发生过:连续六份归档写着「Go 1.23.0 验证通过」,实际全跑在 1.26.5 上。 +新加依赖时尤其要跑——依赖的 `go` 指令高于 1.23.0 的话,只有这条命令能发现。 + - `go run .` 启动后访问 `http://localhost:8080`,**不会连手机、不会下单**,可随时运行。 - 修改数据库时测试首次建库和从上一版本迁移。 + `[必须]` **不要只测全新库。** 先列出现实中存在哪些 schema 状态 + (不同版本的程序建过的库都算),一个个迁过来验。见 `docs/task/20-*.md`。 - 修改给 Client 的接口时,跑契约测试,确认仍满足 Client 侧 §6.1 的无条件接受。 - 修改 Excel 导入时用 `raw_data/` 下的样本跑一遍,核对导入条数。 该样本含商业数据、**不在仓库里**,需向项目负责人索取;自动化测试用 `testdata/` 下的脱敏小样本。 diff --git a/docs/task/16-admin-pdd-商品数据独立成表.md b/docs/task/16-admin-pdd-商品数据独立成表.md index a6f1f4c..e9786b0 100644 --- a/docs/task/16-admin-pdd-商品数据独立成表.md +++ b/docs/task/16-admin-pdd-商品数据独立成表.md @@ -99,7 +99,7 @@ PDD 链接时会各存一份、各采一次;`collect_status` 描述的是 PDD ## 测试 -执行的命令(Go 1.23.0,`admin/` 目录): +执行的命令(`admin/` 目录): ``` go vet ./... 无输出 diff --git a/docs/task/17-admin-claim-支持领取无主任务.md b/docs/task/17-admin-claim-支持领取无主任务.md index d5d97fb..e2788d4 100644 --- a/docs/task/17-admin-claim-支持领取无主任务.md +++ b/docs/task/17-admin-claim-支持领取无主任务.md @@ -94,7 +94,7 @@ Client 侧对这两种**没有任何区别**:调 `claim`,拿到任务就做 ## 测试 -执行的命令(Go 1.23.0,`admin/` 目录): +执行的命令(`admin/` 目录): ``` go vet ./... 无输出 diff --git a/docs/task/18-admin-pdd-商品页面.md b/docs/task/18-admin-pdd-商品页面.md index 106fb95..ecccd93 100644 --- a/docs/task/18-admin-pdd-商品页面.md +++ b/docs/task/18-admin-pdd-商品页面.md @@ -112,7 +112,7 @@ map 遍历,而 map 是无序的——同一个商品每次刷新页面「颜 ## 测试 -架构角色亲自执行(Go 1.23.0,`admin/` 目录): +架构角色亲自执行(`admin/` 目录): ``` go vet ./... 无输出 diff --git a/docs/task/19-admin-采集采购页.md b/docs/task/19-admin-采集采购页.md index e28bad7..a59e858 100644 --- a/docs/task/19-admin-采集采购页.md +++ b/docs/task/19-admin-采集采购页.md @@ -119,7 +119,7 @@ ## 测试 -架构角色亲自执行(Go 1.23.0,`admin/` 目录): +架构角色亲自执行(`admin/` 目录): ``` go vet ./... 无输出 diff --git a/docs/task/20-admin-修复迁移被原地改写.md b/docs/task/20-admin-修复迁移被原地改写.md index 7fea01f..6b4c765 100644 --- a/docs/task/20-admin-修复迁移被原地改写.md +++ b/docs/task/20-admin-修复迁移被原地改写.md @@ -131,7 +131,7 @@ for v := current; v < len(migrations); v++ { ... } ## 测试 -架构角色亲自执行(Go 1.23.0,`admin/` 目录): +架构角色亲自执行(`admin/` 目录): ``` go vet ./... 无输出 diff --git a/docs/task/24-admin-商品卡在采集中无法恢复.md b/docs/task/24-admin-商品卡在采集中无法恢复.md index 3b9d0ae..a86b888 100644 --- a/docs/task/24-admin-商品卡在采集中无法恢复.md +++ b/docs/task/24-admin-商品卡在采集中无法恢复.md @@ -143,7 +143,7 @@ const CollectStaleAfter = 15 * time.Minute ## 测试 -架构角色亲自执行(Go 1.23.0,`admin/` 目录): +架构角色亲自执行(`admin/` 目录): ``` go vet ./... 无输出 diff --git a/docs/task/38-admin-蝦皮excel报表导入.md b/docs/task/38-admin-蝦皮excel报表导入.md new file mode 100644 index 0000000..2dd1ea5 --- /dev/null +++ b/docs/task/38-admin-蝦皮excel报表导入.md @@ -0,0 +1,243 @@ +# 38 Admin 蝦皮 Excel 报表导入 + +- 类型:需求(数据导入) +- 父级大工单:#14 +- 所属 MVP / 版本:#15 / MVP +- 关联:#16(`pdd_products` 独立成表)、#20(迁移只追加) +- 状态:待验收 +- 日期:2026-08-08 +- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/38 + +## 背景与目标 + +蝦皮数据模块整个是骨架,四个入口全部返回 501, +`service.ImportShopeeExcel` 直接 `return nil, ErrNotImplemented`, +`excelize` 连依赖都没装。蝦皮那 5195 个商品一直躺在 Excel 里进不了系统。 + +## 建单前确认的事实(读真实样本得出) + +样本 `raw_data/蝦皮数据样本.xlsx`(1.9MB,gitignore 不进库): + +### ① 工作簿有 5 个 sheet,只有一个有数据 + +| Sheet | 列数 | 数据行 | 規格ID 为 `-` | 有規格ID | +|---|---|---|---|---| +| **最佳表現商品** | **40** | **11287** | **5195** | **6092** | +| 新上架商品 | 36 | 1145 | 0 | 0 | +| 高潛力廣告商品 / 優化商品廣告 / 追蹤商品廣告成效 | 30 | 41 / 35 / 7 | 0 | 0 | + +其余四个是广告报表,**连 `商品規格ID` 列都没有**。 + +### ② 规格原文的真实分布 + +``` +含【】 3252 53.4% 黑色,M【建議40-50公斤】 +逗号+空格 1609 26.4% 卡其色拼黑色,L 建議50-57.5kg +只有逗号 1231 20.2% 黑色,M +``` + +**只认【】会漏掉 46.6%。** + +### ③ 数据比预想干净,但有两类脏数据 + +全部 6092 行**恰好一个逗号**(0 行没逗号、0 行多逗号), +所以「按第一个逗号切开」是安全的,不是碰运气。 + +脏数据共 23 行: + +``` +21 行 括号不配对 紅色,3XL建議80-90公斤】 + 2 行 右半整个被【】包住 面膜安全褲3條膚色,【2xl 70-85公斤可穿】 +``` + +## 最终方案 + +### upsert 白名单式,绝不碰人工字段 + +```sql +INSERT INTO shopee_products (goods_id, title, shopee_status, main_sku_code, created_at, updated_at) +VALUES (?, ?, ?, ?, ?, ?) +ON CONFLICT(goods_id) DO UPDATE SET + title = excluded.title, shopee_status = excluded.shopee_status, + main_sku_code = excluded.main_sku_code, updated_at = excluded.updated_at +``` + +`[必须]` `pdd_goods_url` / `pdd_goods_id` **既不在 INSERT 列清单里, +也不在 DO UPDATE SET 里**。报表没有这两列,写进去就是写空值—— +操作员攒几周的 PDD 链接会被一次导入洗光,而且不报错, +等到建采购任务才发现,那时已经找不回来。 + +`[必须]` 不写 `DELETE + INSERT` 的全量替换,会删掉将来手动新增的 SKU +(`05` §4.4 那个入口的伏笔)。 + +### 按 sheet 名字取,按列名找索引 + +`[必须]` 按名字取「最佳表現商品」,**不得用第 0 个 sheet**。 +它现在恰好是第一个,但蝦皮调顺序时按下标会静默导入一张完全不相干的表。 +找不到要报错并列出实际有哪些 sheet,不退回用第一个。 + +`[必须]` 按列名找索引。40 列里 32 列是统计指标,蝦皮加一列所有列号就错位, +**而且不报错**——会把「點擊率」当成「商品規格」存进去。 + +### 解析失败不猜 + +`[必须]` `ok == false` 时 `color` / `size` / `advice` **必须全为空**, +`parse_ok` 存 0,`spec_raw` 照存原文,界面上标出来让人工补。 + +括号不配对和右半整个被【】包住的,一律**判整体失败**,不逐段硬凑。 +理由(实现写在 `ParseSpec` 注释里,比工单要求的更清楚): +这些脏数据的错法不统一(缺左括号 / 缺右括号 / 用「寬鬆版」代替「建議」), +针对每种错法单独写规则等于在猜,而且**没有人工核对过的正确答案可验证**。 + +## 与建单方案的差异 + +1. **括号不配对判整体失败**(工单允许实现决定,要求写清理由并有测试固定)。 + 实测命中 **23 行**,比建单时统计的 21 行多 2 行——多出的是 + 「右半整个被【】包住、括号外没有尺码文字」这个建单时没单独归类的格式, + 同样归为解析失败。 +2. **`ShopeeList` 无分页**。工单未要求;PDD / 客户端等既有列表页也是单次全量查询, + 保持风格一致。 +3. **导入结果不走 303 跳转,直接渲染页面**。失败行可能几十条, + 塞进跳转的查询参数既丑又有长度限制,无法满足「失败行要能全部看到」。 + 代价是导入后 F5 会触发浏览器「重新提交表单」确认,已在 handler 注释里写明取舍。 + +## 改了哪些 + +- `admin/go.mod` / `go.sum`:加 `github.com/xuri/excelize/v2 v2.9.1`。 +- `admin/service/shopee_import.go`:新建。`ParseSpec` + `ImportShopeeExcel` + + 文件安全校验。 +- `admin/service/shopee_import_test.go`:新建。表驱动解析测试 + 6 个导入场景, + 固件用 excelize 现造(不依赖 `raw_data/`)。 +- `admin/service/shopee_list.go`:新建。列表视图组装。 +- `admin/service/service.go`:删掉骨架版的重复声明。 +- `admin/repository/shopee.go`:新建。白名单式 upsert、列表查询。 +- `admin/handler/web/shopee.go`:`ShopeeList` / `ShopeeImport` 真实实现; + 其余三个保持 501。 +- `admin/templates/shopee/list.html`:表格渲染 + 失败行完整列表。 +- `admin/testdata/README.md`:更新为「固件现造」说明。 + +## 验收结果 + +| 验收标准 | 结果 | +|---|---| +| 三种规格格式都能解析 | 通过 | +| 颜色部分含【】时仍正确 | 通过 | +| 脏数据有确定行为 + 测试固定 + 注释写清理由 | 通过 | +| **`ok=false` 时三个字段全为空** | 通过(5 个失败分支变异全部被抓到) | +| 按 sheet **名字**取,找不到报错列出实际 sheet | 通过 | +| 按**列名**找索引,缺列报错 | 通过 | +| `商品規格ID == "-"` 归商品,其余归 SKU | 通过 | +| **已有的 `pdd_goods_url` / `pdd_goods_id` 导入后仍在** | 通过(有独立测试 + 端到端验证) | +| `is_manual = 1` 的 SKU 不被删除 | 通过(无全量替换) | +| 失败行返回行号 + 原文 + 原因,界面全部可见 | 通过 | +| 流式读,不是 `GetRows()` | 通过 | +| 非 `.xlsx` 拒绝(看内容不只看扩展名) | 通过 | +| 落盘用自己生成的文件名 | 通过(`shopee-.xlsx`) | +| 超过 50MB 拒绝 | 通过 | +| 测试不依赖 `raw_data/` | 通过(固件现造) | +| `ShopeeSave` / `ShopeeDelete` / `ShopeeCollect` 仍是 501 | 通过(3 处) | +| `go vet` / `gofmt` / `go test` 全过 | 通过 | + +## 测试 + +架构角色亲自执行(`admin/` 目录)。**本次特意用固定版本工具链复跑**: + +``` +GOTOOLCHAIN=go1.23.0 go vet ./... 无输出 +GOTOOLCHAIN=go1.23.0 gofmt -l . 无输出 +GOTOOLCHAIN=go1.23.0 go test ./... -count=1 + ok cmautobuy/admin 0.034s + ok cmautobuy/admin/repository 0.669s + ok cmautobuy/admin/service 1.974s +``` + +`excelize v2.9.1` 的 `go` 指令正好是 `1.23.0`,与项目固定版本一致。 + +### 真实样本端到端 + +``` +① 第一次导入 导入完成:5195 个商品 / 6092 个 SKU,23 行解析失败 + DB 实查 shopee_products=5195 shopee_skus=6092 +② 手工填链接 UPDATE shopee_products SET pdd_goods_url='https://...', pdd_goods_id='999' +③ 再导一次 DB 计数不变(5195 / 6092),无重复行 + 链接还在:17197130343 | https://mobile.yangkeduo.com/... | 999 + title 正常刷新 +``` + +### 变异测试(架构角色补做,工单未要求) + +**人工字段保护**——往 `DO UPDATE SET` 里加回 `pdd_goods_url = excluded.pdd_goods_url`: + +``` +--- FAIL: TestImportShopeeExcel_不覆盖人工字段 +``` + +**`ParseSpec` 的 5 个失败分支**,逐个改成 `return "泄漏","泄漏","泄漏", false`: + +| 行 | 分支 | 结果 | +|---|---|---| +| 337 | 空输入 | ✅ 3 个子用例变红 | +| 341 | 括号不配对 | ✅ 3 个 | +| 347 | 逗号数不为 1 | ✅ 3 个 | +| 354 | 逗号一侧为空 | ✅ 3 个 | +| 378 | 提取后尺码为空 | ✅ 2 个 | + +**未验证到的部分:** + +- **Windows / PowerShell 环境未实测**。验证全在 WSL/Linux 完成。 +- **浏览器实机渲染未走查**。只验证了 HTTP 响应和 HTML 内容, + 6092 行表格在 1366×768 下的列宽、横向滚动表现未看过。 +- **打包成 exe 后的行为未验证**(`data/uploads/` 落盘路径)。 + 原有代码路径未改动,理论上不受影响,但没有为本工单重新验证。 + +## 审查过程 + +打回一次。 + +**打回原因:** `ParseSpec` 有两个失败分支没有测试覆盖。 +测试文件里**有**「`ok=false` ⇒ 三个字段必须为空」的断言, +问题是没有用例能走到 `color==""||right==""` 和 `size==""` 这两个分支。 + +其中 `size==""` 尤其要紧——**它就是真实数据里那 2 行走的分支** +(`面膜安全褲3條膚色,【2xl 70-85公斤可穿】`)。如果有人把它改成 +`return right, "", "", false`(看着「更有信息量」),真实导入会给这 2 行 +存进一个乱七八糟的颜色,而且没有任何测试变红,这个值会一路传到 +规格匹配和采购任务。 + +**架构角色的一次误判:** 第一轮变异打在「输入为空」那个分支上, +把 `return "", "", "", false` 改成 `return raw, ...`——`raw` 本来就是空串, +等于没改,测试当然全绿,差点据此报告「测试没牙」。 +逐行重打之后才看清是哪两个分支真的没覆盖。 +**探针无效和代码有问题是两回事**,得先确认变异真的改变了行为。 + +**返工结果:** 补了 4 条用例(含实现自己发现的第 5 个缺口—— +`trimmed==""` 分支原本也没被真正覆盖,唯一的空输入用例 `raw:""` +在变异后 `raw` 仍是 `""`,抓不到;补一条 `raw:" "` 才能暴露)。 +实现代码一字未动。 + +## 遗留问题 + +1. Windows 环境、浏览器实机渲染、exe 打包后的落盘路径均未验证。 +2. `ShopeeSave` / `ShopeeDelete` / `ShopeeCollect` 仍是 501,属后续工单。 +3. 手动新增 SKU(`05` §4.4)未做。蝦皮报表只含**有销售成绩的** SKU + (平均每个商品仅 1.17 个),导入进来的数据本身就是不全的。 + +## 附带修正(不属于本工单范围) + +审查中发现 `/usr/local/go` 从 2026-07-02 起一直是 **1.26.5**, +而 #16 / #17 / #18 / #19 / #20 / #24 六份归档都写着 +「架构角色亲自执行(Go 1.23.0)」——那是照项目固定版本抄的, +**没有实际验证过工具链**,违反 `CLAUDE.md` §9。 + +已做两件事: + +1. 六份归档的措辞改正为不宣称具体版本。 +2. `admin/AGENTS.md` 的验证章节新增 `[必须]`:交付前至少跑一次带 + `GOTOOLCHAIN=go1.23.0` 的验证,并写明本项目发生过什么。 + 同时补了一条「不要只测全新库,先列出现实中存在哪些 schema 状态」(来自 #20)。 + +本工单已按新规则用 1.23.0 复验,结论不变。 + +## 相关提交 + +- `e29d668` feat: 蝦皮 Excel 报表导入 (#38)