# 契约:商品文件夹与 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位序号>[-<规格名>].` | 序号补零保证字典序 = 展示序 | | 规格名 | 保留原文(含俄文/中文),清洗非法字符 | 跟卖时规格名要对应回 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。