feat: 重构顺运宝规格主链路 (#88)
This commit is contained in:
+53
-47
@@ -128,6 +128,12 @@ SQLite 同一时刻只允许一个写事务,连接放太开会互相抢锁、
|
||||
错误要等操作员点到那个页面才暴露,如果那是个写操作页面,暴露出来的就不是报错
|
||||
而是写坏数据。自检只查表名,不逐列校验:够抓住"迁移没跑到"这一类问题,代价也低。
|
||||
|
||||
MySQL 迁移 v3(工单 #88)是追加式迁移:新增 `spec_mappings`,为 `syb_orders`
|
||||
增加 `spec_key`,为 `shopee_products` 增加 `source`。迁移使用稳定 `syb_id`
|
||||
游标分批回填,先完整备份旧 `sku_mappings` 为 `sku_mappings_v3_backup`,
|
||||
再转换能找到顺运宝商品与规格的映射。复跑、字段已建但约束未建等中断状态必须自动收敛;
|
||||
启动自检除表名外还校验 v3 主键、长度、二进制排序规则和 `source` 取值约束。
|
||||
|
||||
## 3. 蝦皮数据
|
||||
|
||||
蝦皮报表**一个文件里混了两层数据**,所以拆成两张表。
|
||||
@@ -140,6 +146,7 @@ CREATE TABLE shopee_products (
|
||||
title TEXT NOT NULL, -- 蝦皮「商品名稱」
|
||||
shopee_status TEXT, -- 蝦皮「商品當前狀態」
|
||||
main_sku_code TEXT, -- 蝦皮「主商品貨號」
|
||||
source VARCHAR(16) COLLATE utf8mb4_bin NOT NULL DEFAULT 'report', -- report / syb
|
||||
|
||||
-- 下面两个是我们自己维护的,报表里没有,导入时绝不能覆盖
|
||||
pdd_goods_url TEXT, -- ★ 人工填写的 PDD 链接原文
|
||||
@@ -149,11 +156,18 @@ CREATE TABLE shopee_products (
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE shopee_products ADD CONSTRAINT chk_shopee_products_source
|
||||
CHECK (source IN ('report', 'syb'));
|
||||
|
||||
CREATE INDEX idx_shopee_products_pdd ON shopee_products(pdd_goods_id);
|
||||
```
|
||||
|
||||
`[必须]` **采集结果和采集状态不在这张表里**,它们属于 PDD 商品,见 §4。
|
||||
|
||||
`source='syb'` 表示顺运宝先到、蝦皮报表尚未导入时创建的最小商品骨架。
|
||||
后续 Excel 导入同一 `goods_id` 时必须补全商品信息并把来源提升为 `report`;
|
||||
导入不得覆盖人工维护的 PDD 关联。
|
||||
|
||||
`pdd_goods_id` 表示"这个蝦皮商品**当前**对应哪个 PDD 商品"。
|
||||
PDD 商品下架换代时改这里,是一个随时会变的关联,不是永久绑定。
|
||||
|
||||
@@ -299,7 +313,7 @@ CREATE INDEX idx_pdd_products_status ON pdd_products(collect_status);
|
||||
|
||||
**为什么用软删除**
|
||||
|
||||
`sku_mappings` 指向这张表。硬删会把人工攒了很久的匹配成果一起带走。
|
||||
`spec_mappings` 指向这张表。硬删会把人工攒了很久的匹配成果一起带走。
|
||||
软删除后界面不再显示,但记录和映射都还在。
|
||||
|
||||
`[必须]` 操作员重新填同一个链接时**要能复活**(清 `deleted_at`、状态置回
|
||||
@@ -434,9 +448,10 @@ CREATE TABLE syb_orders (
|
||||
syb_id TEXT PRIMARY KEY, -- 货运单**明细行** ID(顺运宝 details[].id)
|
||||
order_no TEXT NOT NULL, -- 订单号(顺运宝外层 code,不是 orderCode)
|
||||
title TEXT, -- 商品标题
|
||||
product_spec TEXT, -- 规格原文,v5 新增,对应 shopee_skus.spec_raw
|
||||
product_spec TEXT, -- 顺运宝规格原文
|
||||
spec_key VARCHAR(191), -- 规格身份键;空规格保持 NULL
|
||||
shopee_goods_id TEXT, -- 蝦皮商品 ID(顺运宝 productId,11 位)
|
||||
shopee_sku_id TEXT, -- 蝦皮规格 ID,人工/自动匹配的结果
|
||||
shopee_sku_id TEXT, -- 历史兼容列;新主链路不读写
|
||||
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
||||
price_twd_cent INTEGER CHECK (price_twd_cent IS NULL OR price_twd_cent >= 0),
|
||||
image_url TEXT, -- 存 URL,不存图片本身
|
||||
@@ -453,24 +468,12 @@ CREATE INDEX idx_syb_orders_list ON syb_orders(updated_at DESC, syb_id DESC);
|
||||
- `price_twd_cent` 是**台币分**,是蝦皮那边的售价,
|
||||
和采购任务的人民币价格上限**没有换算关系**,不要互相赋值。
|
||||
- `image_url` 存 URL。`[必须]` 不要把图片二进制存进数据库。
|
||||
- `shopee_sku_id` **直接对应 `shopee_skus.sku_id`**,编号格式一致,不需要额外转换。
|
||||
|
||||
`[必须]` 但**不要加外键约束**。理由:蝦皮报表不是全量目录(样本里平均每商品仅 1.17 个 SKU),
|
||||
货运单来了而本地查不到这个 SKU 是**常态**。加了外键,同步就会直接失败。
|
||||
|
||||
正确做法是软关联:查不到时照常保存货运单,界面上标出"SKU 未收录",
|
||||
并引导操作员到蝦皮数据模块手动新增(见 [05 界面规范](05-ui-specification.md) §4.4)。
|
||||
|
||||
**"编号能对上"和"本地一定查得到"是两回事,别混。**
|
||||
|
||||
**处理阶段是派生的,不存字段**:`shopee_sku_id` 非空只表示蝦皮规格已确认。
|
||||
PDD 规格是否已匹配要联查 `sku_mappings`,同时限定 `shopee_sku_id` 和当前
|
||||
`pdd_goods_id`,并确认 `pdd_option_key` 仍存在于最新 `skus_json`;选项消失后按待匹配处理。
|
||||
|
||||
`[必须]` **`shopee_sku_id` 顺运宝同步绝不能覆盖**(工单 #46)。它是蝦皮规格识别
|
||||
结果(人工确认或确定性唯一识别产生),顺运宝那边根本没有这个值(顺运宝只给商品级 `productId`,
|
||||
不含蝦皮規格ID,见 §5.1 下方接口对照)。`repository.UpsertSybOrder` 的
|
||||
`ON CONFLICT DO UPDATE SET` 里不出现这一列,新建行时才会写它(此时通常是空值)。
|
||||
- `spec_key` 通过 `SpecKey(product_spec)` 计算:去除首尾空白,把连续空白折叠为一个半角空格,
|
||||
但不做大小写、繁简、颜色或单位转换。空规格不生成键,并在采购流程中作为数据异常阻断。
|
||||
- **处理阶段是派生的,不存字段**:PDD 规格是否已匹配要按
|
||||
`(shopee_goods_id, spec_key, 当前 pdd_goods_id)` 联查 `spec_mappings`,并确认 `pdd_option_key`
|
||||
仍存在于最新 `skus_json`。
|
||||
- `shopee_sku_id` 只为兼容历史数据保留;顺运宝同步、弹窗、映射和建采购任务均不再读写它。
|
||||
|
||||
`[必须]` 一行对应顺运宝一张货运单的**一个商品明细**(`details[]` 的一项),
|
||||
不是一张货运单——一张货运单可以有多个商品,各占一行,`syb_id` 用的是
|
||||
@@ -518,33 +521,35 @@ CREATE TABLE syb_sync_state (
|
||||
`[必须]` 本表不保存顺运宝 Cookie、token、密码、验证码、收件信息或原始响应。
|
||||
失败原因最多保留 500 个字符,并在页面输出时由模板转义。
|
||||
|
||||
## 6. `sku_mappings` 规格映射
|
||||
## 6. `spec_mappings` 顺运宝规格映射
|
||||
|
||||
"蝦皮的这个规格 = 拼多多的那个规格",**匹配一次,以后复用**。
|
||||
"这个蝦皮商品的这条顺运宝规格 = 当前拼多多商品的那个规格",**匹配一次,以后复用**。
|
||||
|
||||
```sql
|
||||
CREATE TABLE sku_mappings (
|
||||
shopee_sku_id TEXT NOT NULL,
|
||||
pdd_goods_id TEXT NOT NULL, -- ★ 这条映射属于哪个 PDD 商品
|
||||
pdd_option_key TEXT NOT NULL, -- 规范化的组合键,见 §6.2
|
||||
pdd_options TEXT NOT NULL, -- 原始 options 对象,显示用
|
||||
goods_id TEXT NOT NULL, -- 蝦皮商品 ID,方便按商品批量查
|
||||
mapped_at TEXT NOT NULL,
|
||||
mapped_by TEXT,
|
||||
PRIMARY KEY (shopee_sku_id, pdd_goods_id),
|
||||
FOREIGN KEY (shopee_sku_id) REFERENCES shopee_skus(sku_id) ON DELETE CASCADE
|
||||
CREATE TABLE spec_mappings (
|
||||
shopee_goods_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
spec_key VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
pdd_goods_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
pdd_option_key VARCHAR(191) COLLATE utf8mb4_bin NOT NULL, -- 见 §6.2
|
||||
pdd_options LONGTEXT NOT NULL, -- 原始 options JSON,显示用
|
||||
spec_raw TEXT NOT NULL,
|
||||
mapped_at VARCHAR(35) NOT NULL,
|
||||
mapped_by VARCHAR(191),
|
||||
PRIMARY KEY (shopee_goods_id, spec_key, pdd_goods_id),
|
||||
KEY idx_spec_mappings_goods (shopee_goods_id),
|
||||
KEY idx_spec_mappings_pdd (pdd_goods_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sku_mappings_goods ON sku_mappings(goods_id);
|
||||
CREATE INDEX idx_sku_mappings_pdd ON sku_mappings(pdd_goods_id);
|
||||
```
|
||||
|
||||
### 6.1 为什么主键要带上 `pdd_goods_id`
|
||||
`[必须]` 不加外键。顺运宝明细可能比蝦皮报表更早到达,同步要先创建 skeleton 商品;
|
||||
PDD 商品还可软删除,映射作为审计和可恢复数据必须保留。
|
||||
|
||||
### 6.1 为什么主键要带上商品和 `pdd_goods_id`
|
||||
|
||||
PDD 商品下架换代很频繁——A 买不到了就得换 B。
|
||||
|
||||
假设蝦皮商品 X 原来对应 PDD 商品 A,操作员匹配好了"黑色/M → 黑色/M码";
|
||||
后来 A 下架,换成了 B。如果映射只按 `shopee_sku_id` 存,那条旧映射还在,
|
||||
后来 A 下架,换成了 B。如果映射不带 `pdd_goods_id`,那条旧映射还在,
|
||||
但它描述的是 **A 的规格**:
|
||||
|
||||
| | 后果 |
|
||||
@@ -555,8 +560,9 @@ PDD 商品下架换代很频繁——A 买不到了就得换 B。
|
||||
把 `pdd_goods_id` 放进主键后,`[必须]` 查映射**永远带上"当前对应的 PDD 商品"**:
|
||||
|
||||
```sql
|
||||
SELECT ... FROM sku_mappings
|
||||
WHERE shopee_sku_id = ?
|
||||
SELECT ... FROM spec_mappings
|
||||
WHERE shopee_goods_id = ?
|
||||
AND spec_key = ?
|
||||
AND pdd_goods_id = (蝦皮商品当前的 pdd_goods_id)
|
||||
```
|
||||
|
||||
@@ -701,12 +707,12 @@ CREATE TABLE clients (
|
||||
|
||||
```text
|
||||
shopee_products ──1:N──→ shopee_skus
|
||||
│ │
|
||||
│ pdd_goods_id │ shopee_sku_id
|
||||
│ (当前对应哪个 ↓
|
||||
│ PDD 商品,可换) sku_mappings ──pdd_goods_id──┐
|
||||
↓ │
|
||||
pdd_products ←────────────────────────────────────────┘
|
||||
│
|
||||
│ pdd_goods_id(当前 PDD 商品,可换)
|
||||
↓
|
||||
pdd_products ←─pdd_goods_id─ spec_mappings
|
||||
↑
|
||||
syb_orders ─(shopee_goods_id, spec_key)─┘
|
||||
(skus_json 里是所有规格和价格)
|
||||
|
||||
syb_orders ──创建──→ tasks ──分配──→ clients
|
||||
@@ -717,7 +723,7 @@ users(采购员)──1:N 当前归属──────────┘
|
||||
两条关联都可以变,这是有意的:
|
||||
|
||||
- `shopee_products.pdd_goods_id`:PDD 商品下架换代时改
|
||||
- `sku_mappings` 按 `(蝦皮SKU, PDD商品)` 存:换了商品自然查不到旧映射
|
||||
- `spec_mappings` 按 `(蝦皮商品, 顺运宝规格键, PDD商品)` 存:换了商品自然查不到旧映射
|
||||
|
||||
## 11. 与 Client 数据模型的关系
|
||||
|
||||
|
||||
Reference in New Issue
Block a user