19 KiB
商品编辑与首次上架 API
M3.0 已实现第 1 节的
POST /editor-contexts和GET /editor基础读模型,以及对应数据库归属约束。M3.1 已实现产品基础字段、当前平台文案、SKU 与offer_id的原子草稿保存及双版本并发控制。M3.2 已实现目标店铺候选与切换语义、父/子 SKU 的实物/包装规格继承、规格轴、SKU 复制和当前上架状态读模型。M3.3/M3.3.1 已实现 AI 来源素材的字段级合并、原子保存、复采变化提示与 AI 输入读模型。M3.4 已实现本地类目候选、动态属性 schema/字典、属性草稿与类目切换后的未映射保留。M3.5 在门店 Ozon 凭据验证成功后同步该门店的中文类目树,编辑器只读取当前目标门店缓存;属性和关键词字典仍按需同步。媒体选择、定价、AI 任务与发布仍是冻结契约,尚未实现。
此处的商品始终是采集箱内的 Atlas 草稿。所有平台范围接口要求
X-Atlas-Platform-Id。商品通用资料可以跨平台复用;product_platform_profile、SKU 首次导入状态和在线商品不能跨平台/门店复用。
1. 进入编辑与读取编辑器
POST /api/collection-items/{collection_item_id}/editor-contexts
Headers:Idempotency-Key 必填,页头平台由 header 指定,body 为空。服务端验证采集记录归属、平台可用,并取得或创建该商品在当前平台的 product_platform_profile:
- 首次创建时使用该平台默认店铺作为
target_store_id,并为所有未删除 SKU 建立本平台product_variant_platform_listing; - 当前平台没有默认店铺时返回
422 DEFAULT_STORE_REQUIRED,只给出店铺管理引导,不创建缺少目标门店的 profile; - 已存在 profile 时只读取,绝不因再次进入而覆盖用户已选门店、类目、属性或价格;
- 不创建跨平台 profile,也不改变其他平台的上架状态。
返回:
{ "data": {
"collection_item_id": "uuid",
"collection_item_id": "uuid", "product_id": "uuid", "profile_id": "uuid",
"target_store": { "id": "uuid", "name": "Ozon 主店", "currency_code": "RUB" },
"editor_path": "/products/uuid/editor"
} }
GET /api/products/{product_id}/editor
要求 header,返回当前平台 profile 的完整编辑快照。核心形状:
{ "data": {
"product": {
"id": "uuid", "revision": 12,
"common": { "source_title": "来源标题", "brand": null, "model": null, "barcode": null, "purchase_url": "https://…", "supply_note": null },
"default_physical_spec": { "length_mm": null, "width_mm": null, "height_mm": null, "net_weight_g": null, "status": "missing" },
"default_package_spec": { "length_mm": null, "width_mm": null, "height_mm": null, "shipping_weight_g": null, "status": "missing" }
},
"profile": {
"id": "uuid", "revision": 8, "platform_id": "uuid", "market_code": "RU", "fulfillment": "rfbs",
"target_store": { "id": "uuid", "name": "Ozon 主店", "currency_code": "RUB" },
"title": "俄语标题", "description": null, "tags": [],
"category": { "id": "uuid", "platform_type_id": "123", "source": "source_capture", "name": "收纳箱", "path": "家居 › 收纳 › 收纳箱" },
"listing_summary": { "status": "partially_listed", "detail": "1 个 SKU 已导入" }
},
"variants": [], "attributes": [],
"source_material": {
"title": "用户整理后的来源标题", "description": "用户整理后的来源描述",
"price": { "amount": "99.90", "currency_code": "CNY" }, "brand": "来源品牌",
"attributes": [{ "key": "material", "name": "材质", "value": "棉" }],
"categories": [{ "key": "bags", "name": "箱包" }], "source_snapshot_revision": 3
},
"source_material_state": {
"latest": {},
"overridden_fields": ["title", "description"],
"changed_fields": ["title"],
"base_source_snapshot_revision": 2,
"has_source_updates": true
},
"ai_input": {
"source_material": {}, "default_physical_spec": {}, "default_package_spec": {},
"variants": [{ "variant_id": "uuid", "display_name": "黑色 / M", "specification_values": [],
"effective_physical_spec": {}, "effective_package_spec": {} }]
},
"warnings": []
} }
variants 每项同时返回通用 SKU、合并后实物/包装规格、当前平台 listing 摘要(含 offer_id、价格、首次导入状态、最近脱敏失败原因)及已选媒体。动态属性 schema 由第 3 节的独立接口按当前类目读取。候选和已选媒体只含 asset_id、尺寸、MIME、来源角色与短时预览 URL;不含 object_key 或永久 URL。
source_material 是供页面展示和 AI 使用的有效来源素材,由当前 collection_items.normalized_payload 的最新规范化值与用户明确修改过的顶层字段合并而成。未修改字段随重复采集自动更新;已修改字段继续保留用户值。source_material_state.latest 保留本次最新规范化来源,overridden_fields 表示用户覆盖字段,changed_fields 表示复采后与用户稿不同的覆盖字段,页面可据此提示、逐项恢复或全部采用最新采集值。原始 normalized_payload 与 raw_evidence 始终作为采集证据保留,不因编辑而改变。
ai_input 是后续 AI 任务的服务端输入视图:由上述有效 source_material、父商品默认实物/包装规格,以及每个 SKU 合并继承后的有效规格与规格轴组成。
2. 原子保存草稿与 SKU 规则
PUT /api/products/{product_id}/editor-draft
Headers:If-Match: "{product_revision}:{profile_revision}"、Idempotency-Key 必填。请求是整个编辑页的原子快照:
{
"ai_source_material": {
"title": "用户整理后的来源标题", "description": "用户整理后的来源描述",
"price": { "amount": "99.90", "currency_code": "CNY" }, "brand": "来源品牌",
"attributes": [], "categories": [], "source_snapshot_revision": 3
},
"ai_source_overridden_fields": ["title", "description"],
"common": { "source_title": "商品名称", "brand": "品牌", "model": null, "barcode": null, "purchase_url": "https://…", "supply_note": null,
"default_physical_spec": { "length_mm": "100", "width_mm": "50", "height_mm": "20", "net_weight_g": "300", "status": "confirmed" },
"default_package_spec": { "length_mm": "120", "width_mm": "60", "height_mm": "30", "shipping_weight_g": "400", "status": "confirmed" } },
"profile": { "target_store_id": "uuid", "fulfillment": "rfbs", "title": "Название", "description": "…", "tags": ["tag"],
"store_product_category_id": "uuid", "category_source": "manual" },
"variants": [{
"id": "uuid-or-null", "client_key": "new-row-key", "copy_from_variant_id": null, "display_name": "黑色 / M", "source_sku": null,
"specification_values": [{ "axis_key": "color", "value_key": "black", "value_name": "黑色" }], "purchase_cost_cny": "80.00",
"physical_spec": { "length_mm": null, "width_mm": null, "height_mm": null, "net_weight_g": null, "status": null },
"package_spec": { "length_mm": null, "width_mm": null, "height_mm": null, "shipping_weight_g": null, "status": null },
"offer_id": null,
"media_asset_ids": ["uuid", "uuid"]
}],
"attributes": [{ "scope": "common", "variant_id": null, "platform_attribute_id": "100", "complex_id": "0", "values": ["value"], "language_code": "ru", "source": "manual", "confirmation_status": "confirmed" }]
}
语义与校验:
ai_source_material是客户端当前看到的完整有效素材,ai_source_overridden_fields仅列出用户明确修改的顶层字段。服务端只持久化这些字段的覆盖值,其余字段继续跟随最新采集结果;恢复字段时将其从该数组移除即可。- 来源素材与商品/SKU 草稿在同一事务保存,受
product.revision保护。ai_source_material.source_snapshot_revision必须等于服务端当前采集版本;保存期间发生复采时返回409 SOURCE_SNAPSHOT_CHANGED,客户端应重新读取合并结果并让用户确认,不能用旧页面覆盖新来源。 - 保存来源素材不会修改原始采集证据。
ai_source_overridden_fields只允许title、description、price、brand、attributes、categories,且每项按完整顶层值覆盖,不做数组或对象内部的隐式合并。 profile.store_product_category_id必须是当前目标门店的活动类目;服务端从门店类目记录取得platform_type_id,客户端不能伪造类型 ID。attributes是完整属性草稿集合,属性 ID、复杂属性 ID、值类型、单/多值上限和字典值均由当前门店类目 schema 校验。- 类目切换或平台 schema 移除属性时,原有不再适用的属性不会进入新的发布草稿;服务端将它们写入
unmapped_attributes并在编辑快照中返回。用户删除 SKU 时,其 SKU 属性随 SKU 一起移除,不生成无主未映射属性。 variants是当前完整未删除 SKU 集合。省略既有 SKU 表示软删除;无论当前 SPU 状态都允许新增、复制、删除。copy_from_variant_id仅用于新行,复制通用 SKU 数据但不复制任何平台导入映射,生成的新 SKU 从not_listed开始。- 删除已导入 SKU 只从 Atlas 草稿及当前平台可发布集合移除,不删除、不下架、不修改 Ozon 商品或在线镜像;其他平台历史映射保留。删除后当前平台只剩已导入 SKU 时,SPU 汇总自然为“已上架”。
null的 SKU 规格字段表示继承父商品;包装物流规格的status为missing/ambiguous时,不允许定价确认或首次上架,但仍允许保存草稿。实物规格只影响尺寸图,不用于物流。target_store_id只能是当前工作空间、当前平台的活动店铺。变更目标店铺只更新尚未导入 listing 的将来发布目标;importedlisting 永远保留其首次导入时的店铺快照。- 类目变化后不匹配的旧属性作为未映射草稿返回 warnings,不能自动进入发布 payload。属性由平台 schema 校验
type、必填、字典、多值/复杂属性和is_aspect;客户端不能用任意 JSON 绕过。 media_asset_ids是本 SKU 当前平台的最终选图顺序;首期最多 15 张。格式只允许 JPG/PNG/WebP,单文件最多 20 MB,边长超过 10,000 px 拒绝,短边小于 700 px 不得作为首图并给出警告。第一张是主图。common.model是用户填写的商品型号;采集时允许为空,但每次首次上架前必须非空。offer_id是用户填写的目标平台卖家货号;采集时不得从source_sku自动生成或填入。草稿可为空,但每个待首次上架 SKU 的offer_id必须非空且在同一店铺/平台唯一。
成功返回新 product.revision、profile.revision、创建的 SKU ID/client_key 映射和 warnings。任一字段、属性或媒体无效时返回 422 且不部分保存;版本冲突返回 409 VERSION_CONFLICT,客户端应重新读取编辑快照。
3. 目标店铺、类目与属性
PUT /api/product-platform-profiles/{profile_id}/target-store
Headers:If-Match、Idempotency-Key 必填。Body:{ "target_store_id": "uuid" }。这是编辑器完整保存之外的快捷动作,遵守“仅未导入 SKU 更换未来目标店铺”的规则。
GET /api/product-platform-profiles/{profile_id}/attribute-schema
当前 profile 必须已有类目。返回本地同步的字段 schema:platform_attribute_id、complex_id、名称、value_type、is_required、is_collection、max_value_count、dictionary_id 与 is_aspect。可带 ?category_id={uuid} 预览尚未保存的新类目 schema;该类目仍必须属于当前平台且为活动状态。类别未选返回 422 CATEGORY_REQUIRED。
GET /api/product-platform-profiles/{profile_id}/attribute-dictionaries/{dictionary_id}?q={keyword}&cursor={opaque}
返回本地同步的字典值 { items: [{ value_id, value }], next_cursor };可同样传 category_id 读取尚未保存的新类目。字典必须属于当前类目 schema,否则返回 422 ATTRIBUTE_DICTIONARY_INVALID。首期只读取缓存,不让前端直接持有 Ozon 密钥或直接调用 Ozon。
GET /api/product-platform-profiles/{profile_id}/category-options?q={keyword}
返回当前平台的缓存类目树节点,包含本地 UUID、父节点 UUID、平台外部类目/类型 ID、名称、路径和活动状态。选择类目本身不立即写库,随下一次 editor-draft 原子保存。缓存为空时客户端显示“准备中”,不提供同步按钮;Worker 仅使用显式配置的共享 Ozon 缓存源预热该树。
POST /api/product-platform-profiles/{profile_id}/attribute-schema-syncs?category_id={uuid}
为已选门店活动类目创建 store_category_attribute_sync operation。同步以单类目事务替换该门店 schema;下一次编辑草稿保存会依最新 schema 校验并将不匹配旧值转为未映射草稿。
POST /api/product-platform-profiles/{profile_id}/attribute-dictionaries/{dictionary_id}/syncs?category_id={uuid}&q={keyword}
按关键词(至少两个字符)创建 store_attribute_dictionary_sync operation,Worker 使用该门店的 Ozon 字典搜索接口并 upsert 命中的本地值。不会为打开一个字段而全量下载大字典;前端先读本地缓存,缺少结果时由用户明确选择“从 Ozon 搜索”。
GET /api/product-platform-profiles/{profile_id}/validation
返回 profile 与每个未删除 SKU 的本地首次上架校验:商品型号、类目/属性、标题、目标店铺、用户填写的 offer ID、确认的包装规格、已确认定价快照、图片、格式/数量及平台特有规则。已导入 SKU 单独标记 not_publishable_again,而不是当作校验失败。
4. SKU 定价确认
POST /api/product-platform-profiles/{profile_id}/variants/{variant_id}/pricing-confirms
Headers:Idempotency-Key 必填。Body:
{ "preview_id": "opaque-id", "selected_option_id": "candidate-1", "one_time_overrides": null }
服务端重新解析 SKU 的有效包装规格、验证预览仍未过期且配置/汇率/规则未变化,重新计算后写入 pricing_quotes、全部 pricing_quote_options 和当前平台 listing 的售价/划线价/币种/报价引用。成功返回 201 与不可变报价摘要。
包装规格缺失或 ambiguous、预览失效、候选不属于预览、目标店铺不一致、物流不适用时返回 422,不覆盖既有价格。临时更换方案或运输方式仅写入本次报价,不改门店默认方案。
5. 素材选择与水印派生
媒体选择通常随第 2 节草稿一起保存。为支持图片弹窗的异步“添加水印”,提供:
POST /api/product-variant-platform-listings/{variant_listing_id}/watermark-derivatives
Headers:Idempotency-Key 必填。Body:
{ "source_asset_id": "uuid", "watermark_template_id": "uuid" }
服务端验证 source asset 是该 SKU 当前已选图片、模板属于当前工作空间且活动;固化模板当前版本后创建 media_derivatives 和 Worker 任务,返回 202 及 operation。任务成功后返回独立 output asset;原图和其他 SKU 的选择均不改变。用户在图片弹窗确认后,将 output asset 写入下一次 editor-draft.media_asset_ids 来替换原图位置;可再次选择原图恢复。首期不在发布时自动添加水印。
GET /api/media-derivatives/{derivative_id} 返回任务状态、脱敏错误及成功时的输出素材短时预览;不能泄露 object_key。
6. AI 任务与人工应用
所有 AI 接口都只创建任务并返回 202 + operation/ai_task_id,绝不直接改写草稿或发布数据。
创建 AI 任务前,前端必须先保存当前编辑草稿。服务端从已持久化的 ai_input 组装输入,不接收客户端自行拼装的规格:文案和类目任务可读取全部 SKU 的规格摘要;套图和尺寸图任务只读取目标 SKU 的 effective_physical_spec / effective_package_spec。因此 AI 使用的是用户在编辑页保存后的包装规格与商品规格,而不是采集载荷中的尺寸猜测值。
| 接口 | body 核心字段 | 成功结果 |
|---|---|---|
POST /api/product-platform-profiles/{profile_id}/ai/category-suggestions |
可选 hint |
读取采集证据和当前平台类目树,创建类目候选任务。Ozon 来源默认不自动调用,但用户可主动调用。 |
POST /api/product-platform-profiles/{profile_id}/ai/copy |
fields: [title,tags,description]、可选语言 |
创建当前平台文案候选。首期不生成富内容。 |
POST /api/product-platform-profiles/{profile_id}/ai/suite-plans |
variant_ids 必填、prompt 可选 |
每个目标 SKU 明确绑定,创建套图规划/生成任务。 |
POST /api/product-platform-profiles/{profile_id}/variants/{variant_id}/ai/dimension-images |
可选样式 | 固化该 SKU 合并后的实物规格快照;实物规格缺失/待确认返回 422。 |
GET /api/product-ai-tasks/{ai_task_id} 返回状态、模型/输入摘要、候选输出或脱敏错误。
GET /api/product-ai-tasks/{ai_task_id}/category-suggestions 返回候选类目、置信度、依据、rank。
POST /api/product-ai-tasks/{ai_task_id}/category-suggestions/{suggestion_id}/apply
Headers:If-Match、Idempotency-Key 必填。用户确认后才将候选写入 profile 的类目/类型并标记 category_source=ai_suggestion;随后要求重新读取 schema、审查未映射属性。文案/图片 AI 输出同样由用户在下一次 editor-draft 中明确选择并保存。
7. 部分 SKU 首次上架
POST /api/product-platform-profiles/{profile_id}/publish-jobs
Headers:Idempotency-Key 必填。Body:
{ "variant_ids": ["uuid", "uuid"] }
服务端在创建任务前重新运行第 3 节校验。只允许当前 profile 中状态为 not_listed 或 failed 的未删除 SKU;submitting 返回 409 SKU_IMPORT_IN_PROGRESS,imported 返回 422 SKU_ALREADY_IMPORTED,不得重新提交或把编辑草稿当平台更新。
校验通过后在同一事务创建 product_publish_job 和每个 SKU 的 product_publish_job_item,固化 profile、目标店铺、类目/属性、价格快照、图片访问 URL、offer ID 与平台适配请求,再投递 Worker。返回 202:
{ "data": { "publish_job": { "id": "uuid", "status": "queued", "store_id": "uuid", "item_count": 2 } } }
首期 Ozon /v3/product/import 不传仓库或库存。Worker 按 SKU 保存提交、轮询、成功/失败和平台原因;成功 SKU 状态为 imported 后不能从采集箱再次上架。SPU 的未上架/已部分上架/已上架由当前平台未删除 SKU 汇总,采集箱记录始终保留。
GET /api/product-publish-jobs/{publish_job_id}
返回 job、每个 SKU item 的 queued/submitting/polling/imported/failed/skipped、Ozon task/product ID、时间和可展示的脱敏错误。发布失败不删除草稿或报价;用户修正未导入 SKU 后可以创建新的首次上架任务。在线商品镜像由另一个同步模块维护,不能以此 job 的状态替代 Ozon 当前 visible、库存或审核事实。
8. 已替代的旧草案
本文替代 ../product/api.md 中与商品编辑有关的路径。旧草案保留为历史背景,实施以本文件、pricing.md 与 collection-and-extension.md 为准。