Files
2026-08-11 17:09:23 +08:00

196 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 契约:商品文件夹与 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。