feat: 保存并展示 PDD 店铺与价格采样信息 (#31)
This commit is contained in:
@@ -93,6 +93,7 @@ SQLite 同一时刻只允许一个写事务,连接放太开会互相抢锁、
|
||||
| v1 | 初始表结构:`shopee_products`(当时还带着 `pdd_data`/`collect_status` 等采集字段)、`shopee_skus`、`syb_orders`、`sku_mappings`(当时主键只有 `shopee_sku_id`)、`tasks`、`clients`、`idempotency_keys`。 |
|
||||
| v2 | 新增 `task_claims`(领取历史,见 §8)。 |
|
||||
| v3 | 把 PDD 采集数据从 `shopee_products` 拆到独立的 `pdd_products`(本文档 §4 描述的最终结构);重建 `shopee_products`,去掉已经搬走的四个字段;重建 `sku_mappings`,主键改成 `(shopee_sku_id, pdd_goods_id)`(§6.1 的理由)。 |
|
||||
| v4 | `pdd_products` 增加可空的 `shop_name`;老数据保持 `NULL`。 |
|
||||
|
||||
**v3 为什么丢弃旧 `sku_mappings` 数据(见 #20):** 新主键需要 `pdd_option_key`,
|
||||
这是 Go 的 `service.OptionKey()` 用 `json.Marshal` 算出来的规范化键,SQL 语句
|
||||
@@ -248,6 +249,7 @@ CREATE TABLE pdd_products (
|
||||
goods_id TEXT NOT NULL UNIQUE, -- 从 PDD 链接解析
|
||||
url TEXT NOT NULL, -- 操作员填的链接原文
|
||||
title TEXT, -- 采集回来,人工核对用
|
||||
shop_name TEXT, -- 店铺名;采不到或老数据为 NULL
|
||||
skus_json TEXT, -- 采集结果,结构见 §4.2
|
||||
|
||||
collect_status TEXT NOT NULL DEFAULT 'pending'
|
||||
@@ -358,6 +360,8 @@ UPDATE pdd_products
|
||||
"schema_version": 1,
|
||||
"goods_id": "737116531267",
|
||||
"title": "【现货】西装外套三件套",
|
||||
"shop_name": "XX旗舰店",
|
||||
"price_granularity": "color",
|
||||
"captured_at": "2026-08-07T08:00:00Z",
|
||||
"dimensions": [
|
||||
{"key": "color", "name": "颜色分类"},
|
||||
@@ -365,7 +369,9 @@ UPDATE pdd_products
|
||||
],
|
||||
"skus": [
|
||||
{"options": {"color": "黑色", "size": "M"},
|
||||
"price_cent": 1256, "available": true, "raw_price": "¥12.56"},
|
||||
"price_cent": 1256, "list_price_cent": 1990,
|
||||
"price_observed_at": {"color": "黑色", "size": "M"},
|
||||
"available": true, "raw_price": "券后¥12.56"},
|
||||
{"options": {"color": "白色", "size": "M"},
|
||||
"price_cent": 1256, "available": false, "raw_price": "¥12.56"}
|
||||
]
|
||||
@@ -380,12 +386,18 @@ UPDATE pdd_products
|
||||
| `skus[].price_cent` | 建采购任务时带出 `max_price_cent` | 价格保护填不了,Client 会拒绝执行 |
|
||||
| `skus[].available` | 不给缺货规格建任务 | 白跑一趟,Client 到手机上才发现卖光 |
|
||||
| `goods_id` / `title` | 核对"采的是不是要的那个商品" | 链接跳转、采错商品时静默存错 |
|
||||
| `shop_name` | 核对是否来自目标店铺 | 同标题商品无法区分来源;采不到时允许为空 |
|
||||
| `dimensions` | 界面按顺序渲染下拉框 | Go 的 map 无序,不知道该先显示颜色还是尺码 |
|
||||
| `price_granularity` | 告诉下游价格是逐 SKU 实测还是按颜色推断 | 下单价格保护会把推断价误当实测价 |
|
||||
| `price_observed_at` | 标明读取价格时实际选中的规格组合 | 无法区分哪一行是实测价 |
|
||||
| `raw_price` | 价格解析出错时对账 | 只有数字,出错了没法查 |
|
||||
|
||||
`[必须]` 几条硬规则:
|
||||
|
||||
- **`price_cent` 是整数分**,不是 `12.56` 也不是 `"12.56"`。这个数要参与价格保护比对,是会花钱的判断,禁止浮点。
|
||||
- **`price_cent` 是实付价**(如券后价、折后价),不是划线价;可选的划线价放在 `list_price_cent`。
|
||||
- **`price_granularity` 只能是 `color` 或 `sku`**。`color` 表示同颜色各尺码共用一次采样价;老 Client 不传时继续兼容。
|
||||
- **`price_observed_at` 必须记录读取价格时实际选中的完整组合**。按颜色采样时,同颜色下只有与它完全相同的那一行是实测,其余是推断。
|
||||
- **采不到价格时给 `null`,不要给 0**。Admin 遇到 `null` 当"未知"处理并拒绝建任务,绝不当成 0 元。
|
||||
- **`options` 嵌一层,不平铺 `color`/`size`**。支持任意多个维度,碰到三维商品(颜色/尺码/款式)平铺的结构直接装不下。
|
||||
- **`dimensions` 只给 `key` 和 `name`,不给 values**。values 能从 `skus` 去重推出来,存两份迟早不一致。
|
||||
|
||||
@@ -170,7 +170,10 @@ Idempotency-Key: <task_id>:<attempt_id>:result-v1
|
||||
1. 用 `Idempotency-Key` 查是否处理过 → 处理过就返回上次的结果,**不重复落库**;
|
||||
2. 把 `pdd_data` 写进 `tasks.result_data`;
|
||||
3. 采集任务:校验返回的 `goods_id` 与请求一致(不一致返回 `422 COLLECT_GOODS_MISMATCH`),
|
||||
再写进 `pdd_products.skus_json`,`collect_status` 置 `collected`;
|
||||
将 `shop_name` 写进 `pdd_products.shop_name`,完整 `pdd_data` 写进
|
||||
`pdd_products.skus_json`,`collect_status` 置 `collected`;
|
||||
没有 `shop_name` 或值为空时保留已有店铺名;
|
||||
`price_granularity` 非空时只能是 `color` 或 `sku`;
|
||||
**`skus` 为空要置 `failed` 而不是 `collected`**,见 [03 数据模型](03-data-model.md) §4.3;
|
||||
4. `tasks.status` 置 `succeeded`;
|
||||
5. 刷新客户端 `last_seen_at`;
|
||||
|
||||
@@ -53,6 +53,25 @@
|
||||
- 从没导入过 → "还没有数据,点左上角『导入』开始"
|
||||
- 搜索无结果 → "没有匹配的记录" + 清除条件入口
|
||||
|
||||
### 3.1 搜索框宽度
|
||||
|
||||
`[必须]` 搜索框加 `class="search-narrow"` 收到 30%,让「搜索」按钮紧挨着它。
|
||||
目前 **PDD 商品页、顺运宝数据页、采集采购页**在用;
|
||||
蝦皮数据页和客户端列表页仍是撑满的(暂未统一)。
|
||||
|
||||
`[必须]` **不要去改 `.toolbar input[type="text"]` 那条通用规则**——
|
||||
五个页面共用它,改了会把没打算改的页面一起改掉。
|
||||
|
||||
`[必须]` 搜索表单上的 `grow` 类**要保留**,它负责把「删除」按钮顶到最右侧。
|
||||
去掉的话删除按钮会跑到搜索按钮旁边。
|
||||
|
||||
`[必须]` **不要额外加对齐样式**(`margin-right: auto`、`justify-content` 之类)。
|
||||
输入框不再伸展之后,多余空间自然留在按钮之后,按钮就贴着输入框了;
|
||||
额外加会和 `grow` 叠加出难预料的结果。
|
||||
|
||||
`[建议]` placeholder 要短。收到 30% 之后长文案显示不全——
|
||||
真装不下就缩短文案,**不要把宽度调回去**。
|
||||
|
||||
## 4. 蝦皮数据页
|
||||
|
||||
### 4.1 工具条
|
||||
@@ -153,10 +172,7 @@
|
||||
决定提交到哪个地址),表格里的勾选框只挂一份。
|
||||
为每个操作各复制一套勾选框的话,两套迟早会不同步。
|
||||
|
||||
`[必须]` 搜索框用 `class="search-narrow"` 收到 30%,让「搜索」按钮紧挨着它。
|
||||
**不要去改 `.toolbar input[type="text"]` 那条通用规则**——五个页面共用它,
|
||||
改了会把蝦皮 / 顺运宝 / 任务 / 客户端四页的搜索框一起改掉。
|
||||
搜索表单上的 `grow` 类要保留,它负责把「删除」按钮顶到最右侧。
|
||||
`[必须]` 搜索框用 `class="search-narrow"` 收到 30%,见 [§3.1](#31-搜索框宽度)。
|
||||
|
||||
### 5.2 表格列
|
||||
|
||||
@@ -165,6 +181,7 @@
|
||||
| 勾选 | 值是 `goods_id`,支持批量删除、批量建采集任务 |
|
||||
| 商品 ID | `goods_id` |
|
||||
| 标题 | 采集回来的;未采集时显示"(未采集,采集后自动回填)" |
|
||||
| 店铺 | 采集回来的 `shop_name`;未采到显示 `—`,截断处可查看完整文字 |
|
||||
| PDD 链接 | 截断显示,可点开(`target="_blank"` 要带 `rel="noopener noreferrer"`) |
|
||||
| 采集状态 | 中文文字,**不能只靠颜色**;失败时在下面补一行原因;`collecting` 超过 15 分钟没动静显示「**采集中(超时)**」,见下 |
|
||||
| 规格数 | 从 `skus_json` 算 |
|
||||
@@ -217,15 +234,15 @@
|
||||
│ 商品 ID 737116531267 (只读) │
|
||||
│ PDD 链接 [___________________](可编辑) │
|
||||
│ 标题 西装外套三件套 (只读) │
|
||||
│ 店铺 XX旗舰店 (只读) │
|
||||
│ 采集状态 已采集 │
|
||||
│ 失败原因 — │
|
||||
│ 采集时间 2026-08-07 15:20 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 规格(3 个) │
|
||||
│ 颜色分类 尺码 价格 有货 │
|
||||
│ 黑色 M ¥12.56 是 │
|
||||
│ 白色 M ¥12.56 否 │
|
||||
│ 红色 L 未采到 是 │
|
||||
│ 颜色分类 尺码 价格 价格来源 有货 │
|
||||
│ 黑色 M ¥12.56 ✓ 实测 是 │
|
||||
│ 黑色 L ¥12.56 推断 是 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ [重新采集] [取消] [保存] │
|
||||
└─────────────────────────────────────────────┘
|
||||
@@ -241,6 +258,11 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
|
||||
`price_cent` 为 `null` 时显示"未采到",**不得显示成 ¥0.00**——
|
||||
0 元和采不到价格是两回事,而这个数要参与价格保护比对(会花钱)。
|
||||
|
||||
`[必须]` `price_granularity == "color"` 时,规格表上方显示:
|
||||
“价格按颜色采样,同一颜色下各尺码显示同一价格。标 ✓ 的是实测价,其余为推断值。”
|
||||
规格表增加“价格来源”列;`options` 与 `price_observed_at` 完全相同的行显示
|
||||
“✓ 实测”,其余显示“推断”。状态不能只靠颜色表达。
|
||||
|
||||
`[必须]` 三种"没有规格"要分开显示,处理方式不一样:
|
||||
|
||||
| 情况 | 显示 |
|
||||
@@ -315,11 +337,13 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
|
||||
### 6.1 工具条
|
||||
|
||||
```text
|
||||
[同步] [创建采购任务] 订单号 [________] [搜索] [删除]
|
||||
[同步] [创建采购任务] 订单号 [___] [搜索] [删除]
|
||||
```
|
||||
|
||||
`[待定]` MVP 阶段**同步按钮只做占位**:点击提示"同步功能待接入",不发请求。
|
||||
|
||||
`[必须]` 搜索框宽度见 [§3.1](#31-搜索框宽度)。
|
||||
|
||||
### 6.2 表格列
|
||||
|
||||
☐ / 货运单ID / 订单号 / 商品标题 / 蝦皮商品ID / 规格SKU / 数量 /
|
||||
@@ -384,13 +408,17 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
|
||||
### 7.1 工具条
|
||||
|
||||
```text
|
||||
[类型▾ 全部] [状态▾ 全部] 关键词[____________] [搜索] [删除]
|
||||
[类型▾ 全部] [状态▾ 全部] 关键词[___] [搜索] [删除]
|
||||
```
|
||||
|
||||
- **类型**:全部 / 采集 / 采购。
|
||||
- **状态**:全部 + 7 个状态,见 [01 需求](01-requirements.md) §6.2。
|
||||
- **关键词**:同时匹配任务编号、订单号、PDD 商品 ID。
|
||||
|
||||
`[必须]` 搜索框宽度见 [§3.1](#31-搜索框宽度)。
|
||||
placeholder 写「任务编号 / 订单号 / 商品 ID」,**不要写全「PDD 商品 ID」**——
|
||||
收到 30% 之后装不下,会被截断成看不出意思的半截文字。
|
||||
|
||||
### 7.2 表格列
|
||||
|
||||
☐ / 任务编号 / 类型 / **目标** / 状态 / 客户端 / 更新时间
|
||||
|
||||
@@ -270,7 +270,24 @@ Idempotency-Key: task-id:attempt-id:result-v1
|
||||
"attempt_id": "attempt-uuid",
|
||||
"result_type": "collect",
|
||||
"completed_at": "2026-08-06T08:03:00Z",
|
||||
"pdd_data": {}
|
||||
"pdd_data": {
|
||||
"goods_id": "737116531267",
|
||||
"title": "测试商品",
|
||||
"shop_name": "XX旗舰店",
|
||||
"price_granularity": "color",
|
||||
"dimensions": [
|
||||
{"key": "color", "name": "颜色分类"},
|
||||
{"key": "size", "name": "尺码"}
|
||||
],
|
||||
"skus": [{
|
||||
"options": {"color": "黑色", "size": "M"},
|
||||
"price_cent": 470,
|
||||
"list_price_cent": 1990,
|
||||
"price_observed_at": {"color": "黑色", "size": "M"},
|
||||
"available": true,
|
||||
"raw_price": "折后¥4.7"
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -291,6 +308,9 @@ Idempotency-Key: task-id:attempt-id:result-v1
|
||||
- `[必须]` 相同 `Idempotency-Key` 和相同请求内容必须返回同一业务结果。
|
||||
- `[必须]` 相同键但不同内容返回 `409 IDEMPOTENCY_CONFLICT`。
|
||||
- `[必须]` Client 只有收到 `accepted: true` 后才能把本地任务标为 `succeeded`。
|
||||
- `[必须]` `price_cent` 是实际支付价的整数分;划线价可放在 `list_price_cent`。
|
||||
- `[必须]` `price_granularity` 只能是 `color` 或 `sku`。按颜色采样时填 `color`,并为每条 SKU 填写读取该价格时实际选中的 `price_observed_at`。
|
||||
- `[必须]` `shop_name` 采不到时允许省略或留空;Admin 不得因此拒绝老版本 Client,也不得用空值覆盖已保存的店铺名。
|
||||
|
||||
### 6.1 Admin 必须无条件接受
|
||||
|
||||
|
||||
Reference in New Issue
Block a user