196 lines
6.1 KiB
Markdown
196 lines
6.1 KiB
Markdown
# 契约:商品文件夹与 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。
|