refactor: 切换蝦皮数据到目录接口 (#135)
This commit is contained in:
@@ -161,7 +161,7 @@ syb:
|
||||
| `listen tcp :8080: bind: ...` | 8080 端口被占了 | 换端口:`go run . -port 8081`,或关掉占用的程序 |
|
||||
| 页面全是没样式的白底黑字 | 静态文件没加载到 | 确认是从 `admin/` 目录启动的,静态文件路径是相对的 |
|
||||
| `database is locked` | 有别的程序占着数据库 | 关掉 DB Browser 再试;程序运行时只读打开是可以的 |
|
||||
| 导入 Excel 报错但没说哪一行 | 解析器没带行号 | 这是 bug,报错必须带行号,见 [06](06-quality-security.md) §2 |
|
||||
| PDD Excel 导入报错但没说哪一行 | 解析器没带行号 | 这是 bug,报错必须带行号,见 [06](06-quality-security.md) §2 |
|
||||
|
||||
## 6. 数据库在哪里、怎样配置
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
| upsert | "有就更新、没有就新增",一次操作搞定 | Excel 导入的唯一正确做法,见 §3 |
|
||||
| cgo | Go 调用 C 代码的机制。**用了就需要装 C 编译器** | 本项目**避开它**;MySQL 和历史 SQLite 迁移驱动都是纯 Go |
|
||||
| `modernc.org/sqlite` | 纯 Go 实现的 SQLite,不需要 cgo | 只用于读取历史 `admin.db` 和迁移回归,不进入生产运行时 |
|
||||
| excelize | Go 读写 Excel 的库 | 固定用它读蝦皮报表 |
|
||||
| excelize | Go 读写 Excel 的库 | 固定用它读 PDD 链接批量导入文件;蝦皮数据不再由 Admin 读 Excel |
|
||||
| CSRF | 攻击者诱导你在已登录状态下发出非本意的请求 | 所有写操作都要防,见 [06](06-quality-security.md) §4 |
|
||||
| 参数化查询 | SQL 里用 `?` 占位、值单独传,而不是拼字符串 | 防 SQL 注入的唯一正确做法 |
|
||||
| 商品目录批次 | 第三方脚本一次提交的一组蝦皮、PDD 和关联数据 | 用 `source + batch_id` 做幂等;系统只保存处理摘要,不保存完整请求体 |
|
||||
|
||||
@@ -26,7 +26,7 @@ Admin 的职责是把这三方的数据串起来,最终产出 Client 能执行
|
||||
- **管理员:**首次初始化 Admin,执行全部业务操作,并创建、禁用或重置采购员账号。
|
||||
- **采购员:**登录后执行现有业务操作,但不能管理用户。
|
||||
- **Client:**领取任务、在安卓设备上执行、提交结果。契约见 [Client 侧文档](../client/04-admin-api-contract.md)。
|
||||
- **蝦皮:**只提供 Excel 报表导出,**没有接口对接**。
|
||||
- **蝦皮:**不同来源文件由第三方脚本归一化,再提交到 Admin 商品目录接口。
|
||||
- **顺运宝:**提供货运单,`[待定]` 同步方式待确认,MVP 先做占位按钮。
|
||||
- **拼多多:**由 Client 操作,Admin 不直接接触。
|
||||
|
||||
@@ -35,7 +35,7 @@ Admin 的职责是把这三方的数据串起来,最终产出 Client 能执行
|
||||
这条链路是理解全部五个模块的关键,先看懂它:
|
||||
|
||||
```text
|
||||
蝦皮报表 ──导入──→ 商品表 + SKU 表
|
||||
第三方商品目录脚本 ──批量接口──→ 商品表 + SKU 表
|
||||
│
|
||||
编辑弹窗:人工填 PDD 链接
|
||||
│
|
||||
@@ -453,7 +453,7 @@ MVP 包含:
|
||||
|
||||
- 五模块页面框架和统一的三段式布局;
|
||||
- MySQL 8.4 建库与追加式迁移;SQLite 仅作为一次性历史数据迁移来源;
|
||||
- 蝦皮 Excel 导入(upsert)、搜索、批量删除、手动新增、编辑弹窗;
|
||||
- 商品目录批量接口(蝦皮/PDD/关联 upsert)、导入记录、蝦皮搜索、批量删除、手动新增、编辑弹窗;
|
||||
- PDD 链接录入与发起采集;
|
||||
- 顺运宝数据的**手工录入或造数**(同步按钮占位);
|
||||
- 规格匹配弹窗与可复用映射;
|
||||
@@ -485,7 +485,7 @@ MVP 之后:
|
||||
|
||||
- 集中部署,操作人员规模是个位数;不追求大规模并发,但任务领取必须保证并发唯一。
|
||||
- 页面在 1366×768 上可正常使用。
|
||||
- 导入 1 万行 Excel 应在可接受时间内完成,并显示进度或结果统计。
|
||||
- 商品目录批次和 PDD Excel 导入应在可接受时间内完成,并显示结果统计。
|
||||
- 所有写操作有 CSRF 防护,所有 SQL 参数化。
|
||||
- 密码只保存成熟算法生成的哈希;Session 有过期、退出和账号禁用失效机制。
|
||||
- 首次管理员、采购员初始密码和重置密码统一要求至少 6 个字符,最多 72 个字节;
|
||||
|
||||
@@ -90,7 +90,7 @@ admin/
|
||||
│ └── api/ 给 Client 的接口
|
||||
│ └── client_api.go
|
||||
├── service/
|
||||
│ ├── shopee_import.go Excel 解析与 upsert
|
||||
│ ├── catalog_import.go 商品目录批次校验、幂等与 upsert
|
||||
│ ├── collect.go 创建采集任务
|
||||
│ ├── mapping.go SKU 映射复用
|
||||
│ ├── purchase.go 创建采购任务与校验
|
||||
@@ -217,28 +217,27 @@ func DataDir() (string, error) {
|
||||
|
||||
## 7. 请求流程示例
|
||||
|
||||
以"导入蝦皮 Excel"为例,看清各层职责:
|
||||
以“第三方脚本导入商品目录”为例,看清各层职责:
|
||||
|
||||
```text
|
||||
浏览器 POST /shopee/import (multipart 文件)
|
||||
第三方脚本 POST /api/v1/integrations/catalog/batches(JSON)
|
||||
↓
|
||||
handler/web/shopee.go
|
||||
- 校验文件大小和扩展名
|
||||
- 存到 data/uploads/
|
||||
- 调 service.ImportShopeeExcel(path)
|
||||
handler/integration/catalog_api.go
|
||||
- 独立 Bearer 鉴权
|
||||
- 限制请求体并解析 JSON
|
||||
- 调 service.ImportCatalogBatch(...)
|
||||
↓
|
||||
service/shopee_import.go
|
||||
- 用 excelize 逐行读
|
||||
- 按 商品規格ID 是否为 "-" 分成商品行/SKU 行
|
||||
- 解析规格原文 → 颜色/尺码/建议(失败留空)
|
||||
service/catalog_import.go
|
||||
- 校验批次、商品、SKU、金额和关联
|
||||
- 开单事务并处理幂等/冲突
|
||||
- 调 repository 做 upsert
|
||||
- 返回 {商品数, SKU数, 失败行号列表}
|
||||
- 返回各类新增、更新和关联计数
|
||||
↓
|
||||
repository/shopee.go
|
||||
- INSERT ... ON CONFLICT(...) DO UPDATE
|
||||
- 只更新报表来的字段,人工填的 pdd_goods_url 不动
|
||||
repository/catalog_import.go
|
||||
- 按来源观测时间更新
|
||||
- 人工 pdd_goods_url / pdd_goods_id 和 is_manual 不动
|
||||
↓
|
||||
handler 渲染结果页:导入 5195 商品 / 6092 SKU,失败 0 行
|
||||
handler 返回批次 JSON;管理员在统一导入记录页查看摘要
|
||||
```
|
||||
|
||||
`[必须]` 注意最后一步:**upsert 时不得覆盖人工维护的字段**。
|
||||
|
||||
@@ -209,79 +209,17 @@ CREATE INDEX idx_shopee_skus_goods ON shopee_skus(goods_id);
|
||||
CREATE INDEX idx_shopee_skus_parse ON shopee_skus(parse_ok);
|
||||
```
|
||||
|
||||
### 3.3 Excel 导入规则
|
||||
### 3.3 商品目录接口导入规则
|
||||
|
||||
`[必须]` 这一节的每一条都要照做,写错会丢数据。
|
||||
蝦皮页面不再上传或解析固定格式 Excel。第三方脚本负责把不同来源文件归一化,
|
||||
再按 [商品目录接口契约](10-商品目录接入接口.md) 提交结构化商品、SKU、PDD 和关联。
|
||||
|
||||
**第一步:分行**
|
||||
|
||||
| 行类型 | 判断方法 | 样本条数 | 导入到 |
|
||||
|---|---|---|---|
|
||||
| 商品汇总行 | `商品規格ID` 是 `-` 或空 | 5195 | `shopee_products` |
|
||||
| SKU 行 | `商品規格ID` 是数字 | 6092 | `shopee_skus` |
|
||||
|
||||
拿参考样本(11287 行)导入应得到 **5195 商品 + 6092 SKU**,数字对不上就是解析有问题。
|
||||
|
||||
> 样本文件含商业数据,**不在仓库里**,找项目负责人要,放 `raw_data/` 下。
|
||||
> 自动化测试用 `admin/testdata/` 里的小样本,别读大文件。
|
||||
|
||||
**第二步:按列名找索引,不要写死列号**
|
||||
|
||||
报表有 40 列,蝦皮改一次导出格式列号就变。启动时按表头文字定位:
|
||||
|
||||
```go
|
||||
idx := map[string]int{}
|
||||
for i, name := range header {
|
||||
idx[strings.TrimSpace(name)] = i
|
||||
}
|
||||
goodsID := row[idx["商品ID"]]
|
||||
```
|
||||
|
||||
找不到必需列时**直接报错停止**,不要用默认值蒙混过去。
|
||||
|
||||
**第三步:解析规格原文**
|
||||
|
||||
`商品規格` 这一列格式**不统一**,实测两种各占一半:
|
||||
|
||||
| 格式 | 占比 | 样例 |
|
||||
|---|---|---|
|
||||
| 有【】 | 53.4% | `黑色,M【建議40-50公斤】` |
|
||||
| 无括号、空格分隔 | 46.6% | `卡其色拼黑色,L 建議50-57.5kg` |
|
||||
|
||||
好消息:**逗号数恒为 1**(6092 条无例外),所以"颜色,尺码"这个二分结构是稳的。
|
||||
|
||||
规则:
|
||||
|
||||
1. 按第一个逗号切开 → 左边是颜色,右边是"尺码 + 可能的建议";
|
||||
2. 右边尝试提取建议:先找 `【建議...】`,再找 ` 建議...`;
|
||||
3. 剩下的就是尺码;
|
||||
4. `[必须]` 任何一步失败都**不要猜**,把 `parse_ok` 置 0,颜色尺码留空,
|
||||
`spec_raw` 照常保存。界面上把这些行标出来让人工补。
|
||||
|
||||
**第四步: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_data / collect_status 一个都不在这里
|
||||
```
|
||||
|
||||
| 别这么做 | 后果 |
|
||||
|---|---|
|
||||
| `DELETE FROM shopee_products` 再导入 | **人工填的 PDD 链接、采集结果全没了** |
|
||||
| `DO UPDATE SET` 里写 `pdd_goods_url = excluded.pdd_goods_url` | 报表里没这列,会被更新成空 |
|
||||
| 删掉报表里没出现的 SKU | 人工新增的(`is_manual=1`)会被误删 |
|
||||
|
||||
**第五步:返回统计**
|
||||
|
||||
导入结束返回 `{商品数, SKU数, 解析失败行号列表}`,页面上显示出来。
|
||||
**不要静默跳过失败行。**
|
||||
- `[必须]` 整批只做 upsert,不清空表,也不删除批次中缺席的数据。
|
||||
- `[必须]` `spec_raw` 原样保留;解析失败由上游明确提交 `parse_ok=false`,Admin 不猜。
|
||||
- `[必须]` 商品更新不覆盖 `pdd_goods_url` / `pdd_goods_id`,SKU 更新不覆盖 `is_manual`。
|
||||
- `[必须]` 较旧 `observed_at` 不覆盖较新接口数据。
|
||||
- 空 PDD 关联可以建立;相同关联幂等;不同关联返回冲突,不静默换品。
|
||||
- 一个批次在单事务中写入并返回新增、更新、未变化和失败统计。
|
||||
|
||||
## 4. `pdd_products` 拼多多商品
|
||||
|
||||
|
||||
@@ -124,12 +124,11 @@
|
||||
### 4.1 工具条
|
||||
|
||||
```text
|
||||
[导入 Excel] 状态[全部▾] 商品ID [________] [搜索] [删除]
|
||||
[导入记录] 状态[全部▾] 商品ID [________] [搜索] [删除]
|
||||
```
|
||||
|
||||
- **导入 Excel**:选文件 → POST 上传 → 显示结果统计
|
||||
(导入 5195 商品 / 6092 SKU,失败 N 行)。
|
||||
`[必须]` 失败行要列出行号和原因,不能静默跳过。
|
||||
- **导入记录**:管理员进入统一商品目录导入记录页;采购员无系统级审计入口。
|
||||
蝦皮数据由第三方脚本通过商品目录接口提交,本页不再上传或解析 Excel。
|
||||
- **状态筛选**(`[必须]`,工单 #43):全部 / 待补规格 / 未填 PDD 链接 / 已填链接。
|
||||
这个筛选是**刚需**,不是锦上添花——本页主要用途是维护
|
||||
(找出需要补规格的商品、找出还没关联 PDD 链接的商品),只靠商品 ID
|
||||
|
||||
@@ -20,10 +20,8 @@
|
||||
|
||||
`[必须]` 覆盖:
|
||||
|
||||
- **规格原文解析**:两种格式各占一半,两种都要有用例,
|
||||
再加解析失败的用例(确认 `parse_ok=0` 且不瞎猜);
|
||||
- **Excel 导入分行**:商品汇总行 vs SKU 行的判断;
|
||||
- **upsert 不覆盖人工字段**:先填 PDD 链接,再导入一次,断言链接还在;
|
||||
- **商品目录批次**:鉴权、原子写入、幂等重放、关联冲突和旧观测时间保护;
|
||||
- **upsert 不覆盖人工字段**:先填 PDD 链接,再通过目录接口导入,断言链接还在;
|
||||
- **PDD 链接批量导入**:空工作表、正确/错误表头、两个允许域名、短链/非 PDD
|
||||
链接、空行、5000 条边界、文件内重复、新建/复用/复活分类和重复导入不覆盖采集结果;
|
||||
- 任务创建校验:商品、采集结果、有效映射、数量、人民币价格上限、客户端可见范围逐条覆盖;
|
||||
@@ -163,7 +161,7 @@
|
||||
|
||||
`[建议]` 记录这些事件:
|
||||
|
||||
- `shopee_import_started/completed/failed`(带条数统计)
|
||||
- `catalog_import_started/completed/failed`(只带来源、批次号和计数摘要)
|
||||
- `collect_task_created`
|
||||
- `purchase_task_created`
|
||||
- `task_claimed`(带 client_id、task_id)
|
||||
|
||||
@@ -369,23 +369,20 @@ POST /am/stock/detail/listByStock?hist=0
|
||||
### 6.3 `productSpec` 的格式和蝦皮报表完全一致
|
||||
|
||||
```text
|
||||
蝦皮 Excel「商品規格」 黑色,M【建議40-50公斤】
|
||||
蝦皮目录 `spec_raw` 黑色,M【建議40-50公斤】
|
||||
顺运宝 productSpec 白色,L【建議50-60公斤】
|
||||
黑色+白色【純棉兩件裝】 簡約親膚,L【建議52.5-60公斤】
|
||||
```
|
||||
|
||||
`[必须]` **直接复用 `service.ParseSpec()`**(#38 实现),不要另写一份解析。
|
||||
两处各写一份,规则迟早不一致,而解析出的颜色尺码要拿去下单。
|
||||
|
||||
`[必须]` 第二个例子里颜色部分自带 `【】`,`ParseSpec` 已覆盖这种情况
|
||||
(按第一个逗号切开)。
|
||||
`[必须]` 规格身份统一复用 `spec.SpecKey()`:只折叠空白,不猜颜色、尺码或建议。
|
||||
第三方目录脚本提交 `spec_raw` 和明确的解析结果;顺运宝匹配不到时交给人工确认。
|
||||
|
||||
### 6.4 由此推导出的匹配路径
|
||||
|
||||
```text
|
||||
productId ──→ shopee_products.goods_id 商品级,直接相等
|
||||
productSpec ──→ ParseSpec() ──→ 颜色 + 尺码
|
||||
──→ 在该商品的 shopee_skus 里比对 ──→ 拿到 sku_id
|
||||
productSpec ──→ SpecKey() ──→ 稳定规格身份键
|
||||
──→ 在该商品目录和人工映射中查找
|
||||
```
|
||||
|
||||
`[必须]` 比对不上时**不要猜**,标成待人工匹配。蝦皮报表只含有销售成绩的
|
||||
@@ -543,4 +540,4 @@ syb:
|
||||
- 数据落库:[03 数据模型](03-data-model.md)
|
||||
- 界面:[05 界面规范](05-ui-specification.md) §6 顺运宝数据页
|
||||
- 安全要求:[06 质量与安全](06-quality-security.md)
|
||||
- 规格解析:`admin/service/shopee_import.go` 的 `ParseSpec()`
|
||||
- 规格身份:`admin/spec/speckey.go` 的 `SpecKey()`
|
||||
|
||||
Reference in New Issue
Block a user