feat: 开发采集插件

This commit is contained in:
R524809
2026-08-11 17:09:23 +08:00
parent 6c4356de24
commit b27e42dc75
148 changed files with 6898 additions and 9691 deletions
+195
View File
@@ -0,0 +1,195 @@
# 契约:商品文件夹与 product.json
> 状态:设计(待确认)
> 上游:[总体架构 §3](../architecture.md)
> Ozon API[ProductAPI_ImportProductsV3](https://docs.ozon.ru/api/seller/zh/#operation/ProductAPI_ImportProductsV3)
插件、发布工作台、服务端三方唯一的耦合点。改这份文档等于改三端接口。
---
## 1. 文件夹结构
```
<商品名>/
├── product.json
├── sources.json
└── images/
├── main/ main-001.jpg …
├── sku/ sku-001-синий.jpg …
├── detail/ detail-001.jpg …
└── video/ video-001.mp4
```
命名规则:
| 项 | 规则 | 理由 |
|---|---|---|
| 文件夹名 | 商品名清洗后取前 80 字符 | 保留可读性,避开文件系统长度限制 |
| 图片文件名 | `<组>-<3位序号>[-<规格名>].<ext>` | 序号补零保证字典序 = 展示序 |
| 规格名 | 保留原文(含俄文/中文),清洗非法字符 | 跟卖时规格名要对应回 Ozon 变体 |
| 非法字符 | `< > : " / \ | ? *``_` | Windows 兼容 |
序号从 1 开始,**按页面上的出现顺序**,不重排。main-001 即主图第一张,通常就是 Ozon 的封面图。
---
## 2. product.json
```jsonc
{
"_meta": {
"schemaVersion": 1,
"stage": "collected", // collected | edited | published
"createdAt": "2026-08-11T06:12:00Z",
"updatedAt": "2026-08-11T06:12:00Z"
},
// ── Ozon 字段(对齐 ImportProductsV3)──
"offer_id": "", // 自己的货号,采集阶段必空
"name": "Термокружка детская 316",
"description": "…",
"description_category_id": null,
"type_id": null,
"price": "1290", // 采到的竞品价,仅参考
"old_price": "",
"currency_code": "RUB",
"vat": "0",
"depth": null, "width": null, "height": null,
"dimension_unit": "mm",
"weight": null,
"weight_unit": "g",
"images": [], // 发布时才填公网 URL
"primary_image": "",
"images360": [],
"color_image": "",
"attributes": [], // 需类目字典映射,见 §4
"complex_attributes": [],
// ── 本地扩展字段(下划线前缀,提交 Ozon 前剥离)──
"_images": {
"main": [{ "file": "images/main/main-001.jpg", "sourceUrl": "https://…", "w": 1200, "h": 1200 }],
"sku": [{ "file": "images/sku/sku-001-синий.jpg", "variantName": "синий", "sourceUrl": "https://…" }],
"detail": [],
"video": []
},
"_raw": {
"title": "Термокружка детская 316",
"price": "1 290 ₽",
"params": [{ "key": "Материал", "value": "Нержавеющая сталь" }],
"desc": "…",
"sellingPoints": "…"
},
"_pricing": null // studio 计价结果,结构见 §5
}
```
### 2.1 为什么分 Ozon 字段 / `_` 扩展字段
提交 Ozon 时把所有 `_` 开头的键剥掉,剩下的**就是**请求体的 `items[0]`。这样避免了维护两套结构和一层映射代码。
`_raw` 保留采集原文:`name` 会被工作台改写(翻译/优化),改坏了要能回溯原始值。
---
## 3. stage 状态机
```
collected ──(工作台编辑)──> edited ──(发布成功)──> published
```
| stage | 谁写 | 必须满足 |
|---|---|---|
| `collected` | 插件 | `name` 非空,`_images` 至少一张 main |
| `edited` | 工作台 | `offer_id``description_category_id`、尺寸重量、`_pricing` 均已填 |
| `published` | 服务端 | 追加 `_ozon.product_id``_ozon.publishedAt` |
工作台导入时按 stage 决定界面:`collected` 走完整编辑流程,`edited` 直接进复核,`published` 只读 + 提示"已发布"。
---
## 4. attributes 的处理边界
**插件不碰 `attributes`。** Ozon 的属性需要 `{ id, complex_id, values[{ dictionary_value_id | value }] }`,其中 `id``dictionary_value_id` 都得查类目属性字典(`/v1/description-category/attribute``/v1/description-category/attribute/values`),而字典依赖类目——采集阶段还不知道类目。
所以:
```
插件 → _raw.params 存原始 kv 文本
工作台 → 定类目 → 拉字典 → 映射成 attributes
服务端 → 提交前按字典校验必填项
```
映射交互(自动匹配 + 人工确认未匹配项)属于 studio 设计范围,见 `docs/studio/`
---
## 5. _pricing
复用 v1 计价器(`web/js/app.js`)的公式,结构对齐它现有的输出:
```jsonc
"_pricing": {
"purchasePrice": 18.5, // 进货价 ¥
"profitRate": 30, // 净利率 %
"logisticsLevel": "high", // low | high | high2
"weightG": 320,
"dims": { "l": 12, "w": 8, "h": 20 },
"logisticsFee": 0,
"fullCommission": 0,
"totalCost": 0,
"sellingPriceCny": 0,
"sellingPriceRub": 0,
"discountReserve": 50,
"fxRate": 11.8,
"calculatedAt": "2026-08-11T06:30:00Z"
}
```
字段名沿用 v1 页面里的 id 命名,方便对照。
---
## 6. sources.json
```jsonc
{
"sources": [
{
"platform": "ozon",
"itemId": "123456789",
"url": "https://www.ozon.ru/product/…",
"collectedAt": "2026-08-11T06:12:00Z",
"counts": { "main": 6, "sku": 4, "detail": 9, "video": 0 }
},
{
"platform": "1688",
"itemId": "987654321",
"url": "https://detail.1688.com/offer/987654321.html",
"collectedAt": "2026-08-11T07:40:00Z",
"counts": { "main": 5, "detail": 12 }
}
],
"dedupeKeys": ["https://cdn1.ozon.ru/…", "…"]
}
```
`dedupeKeys` 是已采集图片的归一化 URL 指纹。二次采集时插件读这个文件,命中的图标记「已收集」并默认不勾选——这是跨平台追加采集不重复的机制,纯本地实现,不需要服务端。
---
## 7. 双语言实现
| 端 | 位置 | 角色 |
|---|---|---|
| Python | `server/schemas/product.py` | **真源**Pydantic 模型 + 运行时校验 |
| TS | `packages/schema/src/product.ts` | 手写 interface,与真源对齐 |
| fixture | `packages/schema/fixtures/*.json` | 两端都跑一遍,防漂移 |
`schemaVersion` 变更时两端同步改,fixture 加一份新版本样例。当前 v1。