297 lines
25 KiB
Markdown
297 lines
25 KiB
Markdown
# 商品编辑与首次上架 API
|
||
|
||
> M3.0~M3.4 已实现编辑上下文、草稿、SKU、来源素材、类目和动态属性。平台目录刷新方案第 1~4 步已把类目树、类型属性、字典读取及发布预检迁至平台级缓存:编辑器按需刷新,首次发布预检只刷新当前类型和实际使用的字典。素材选择已实现;定价确认、AI 任务、真正调用 Ozon 的首次发布 job 仍是冻结契约,尚未实现。
|
||
|
||
> 此处的商品始终是采集箱内的 Atlas 草稿。所有平台范围接口要求 `X-Atlas-Platform-Id`。商品通用资料可以跨平台复用;`product_platform_profile`、SKU 首次导入状态和在线商品不能跨平台/门店复用。
|
||
|
||
> 架构迁移说明:2026-09-18 已决定将类目/商品类型树、属性定义和字典候选值迁至[平台级参考目录](../architecture/platform-product-reference-catalog.md),同一平台所有门店共用,不维护门店差异层。迁移已完成到商品编辑和发布预检;旧门店级目录表仅保留兼容/回滚,不再作为读取、保存或发布校验事实。
|
||
|
||
## 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,也不改变其他平台的上架状态。
|
||
|
||
返回:
|
||
|
||
```json
|
||
{ "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 的完整编辑快照。核心形状:
|
||
|
||
```json
|
||
{ "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`、价格、首次导入状态、最近脱敏失败原因)、SKU 覆盖的 `tags` / `description` 及已选媒体。SKU 的 `tags: []`、`description: null` 分别表示继承 SPU 主题标签、产品描述;动态属性 schema 由第 3 节的独立接口按当前类目读取。候选和已选媒体只含 `asset_id`、尺寸、MIME、来源角色与短时预览 URL;不含 `object_key` 或永久 URL。
|
||
|
||
`default_package_spec` 是 SPU 的存储事实,`variants[].package_spec` 仅保存该 SKU 的覆盖值;`effective_package_spec` 按“SKU 覆盖值优先、SPU 默认值回退”解析。Ozon 发布适配器在创建 job 时才将每个目标 SKU 的有效值物化为独立 `items[]`:`length_mm → depth`、`width_mm → width`、`height_mm → height`、`shipping_weight_g → weight`,并固定带上 `dimension_unit: "mm"` 与 `weight_unit: "g"`。不得为了 payload 预先将 SPU 默认值复制到所有 SKU 草稿。商品净重/尺寸不是这组固定物流字段;它们的 SPU/SKU 展示归属由当前 Ozon schema 的 `is_aspect` 决定。
|
||
|
||
`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` 必填。请求是整个编辑页的原子快照:
|
||
|
||
```json
|
||
{
|
||
"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"],
|
||
"platform_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, "tags": ["tag"], "description": "该 SKU 的描述",
|
||
"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.platform_product_category_id` 必须是当前平台的活动类目;同平台门店共享类目、类型属性和字典缓存。服务端从平台类目记录取得 `platform_type_id`,客户端不能伪造类型 ID。过渡期仍接受 `store_product_category_id`,但只用于按 `description_category_id + type_id` 映射平台类目,不参与当前读取和校验。
|
||
- 类目切换或平台 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` 时,不允许定价确认或首次上架,但仍允许保存草稿。发布前服务端必须对每个待发布 SKU 解析 `effective_package_spec`,确认长、宽、高、重量均存在且大于 0 后才可生成 Ozon item。实物规格不用于物流;是否向 SPU 或 SKU 展示由其对应的类目 schema 属性决定,尺寸图仍可使用已确认的有效实物规格。
|
||
- `target_store_id` 只能是当前工作空间、当前平台的活动店铺。变更目标店铺只更新**尚未导入** listing 的将来发布目标;`imported` listing 永远保留其首次导入时的店铺快照。
|
||
- 类目变化后不匹配的旧属性作为未映射草稿返回 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 必须已有平台类目。响应包含 `{ items, cache }`;字段 schema 额外返回 Atlas `attribute_id`,供字典接口精确限定属性作用域,其余字段包括 `platform_attribute_id`、`complex_id`、名称、`value_type`、`is_required`、`is_collection`、`max_value_count`、`dictionary_id`、`is_aspect` 与 `editor_field`。`editor_field` 只按 Ozon 属性 ID 计算,不允许按名称或翻译文本匹配:品牌 `85 → brand`、型号名称 `9048 → model`、类型 `8229 → category`、主题标签 `23171 → tags`、JSON 富内容 `11254 → rich_content`、简介/产品描述 `4191 → description`,其他 ID 返回 `null`。可带 `?category_id={platform_category_uuid}` 预览尚未保存的新类目 schema。
|
||
|
||
`cache.status` 为 `fresh | stale | expired | missing`,并返回 `last_success_at`、`fresh_until`、`expires_at`、`is_complete`、`refresh_run_id`、`refresh_status` 和 `retry_after_ms`。fresh 直接返回;stale 返回旧 schema 并创建或复用低优先级平台任务;expired/missing 创建或复用高优先级平台任务并最多等待 5 秒,仍未完成时以 HTTP `202` 返回空 `items` 和缓存状态。API 不直连 Ozon。
|
||
|
||
`GET /api/product-platform-profiles/{profile_id}/attribute-dictionaries/{attribute_id}?q={keyword}&cursor={opaque}`
|
||
|
||
返回 `{ items: [{ value_id, value }], next_cursor, cache }`,每页最多 200 项。`attribute_id` 是 schema 返回的 Atlas 平台属性 UUID,不再只用可能跨类型重复的 Ozon `dictionary_id` 定位。完整字典使用字典整体状态;部分字典本地搜索未命中时,规范化关键词使用独立 `dictionary_search` 状态。fresh/stale/expired/missing 的返回、优先级和 5 秒等待规则与 schema 相同;expired/missing 的旧候选值不作为当前选项返回。
|
||
|
||
`GET /api/product-platform-profiles/{profile_id}/category-options?q={keyword}`
|
||
|
||
返回当前平台共享的缓存类目树节点,包含平台类目 UUID、父节点 UUID、Ozon `description_category_id` / `type_id`、名称、路径和活动状态。接口不再接收或使用 `store_id`;切换同平台目标门店不会清空类目。树为空时客户端提示联系系统管理员同步。
|
||
|
||
`POST /api/product-platform-profiles/{profile_id}/attribute-schema-syncs?category_id={uuid}`
|
||
|
||
兼容入口:为平台类目创建或复用高优先级 `platform_catalog_sync_runs`。普通编辑器读取已自动按新鲜度触发刷新,不要求用户先点击本接口。
|
||
|
||
`POST /api/product-platform-profiles/{profile_id}/attribute-dictionaries/{attribute_id}/syncs?category_id={uuid}&q={keyword}`
|
||
|
||
兼容入口:不传 `q` 时刷新该平台属性的字典批次;传入至少两个字符的 `q` 时刷新独立关键词作用域。任务写入平台 Outbox,由 Worker 使用系统后台配置的平台参考源凭据调用 Ozon,响应不会携带密钥。
|
||
|
||
`GET /api/product-platform-profiles/{profile_id}/validation`
|
||
|
||
返回 profile 与每个未删除 SKU 的本地首次上架校验:标题、型号、品牌、类目/属性、目标店铺、用户填写的 offer ID、确认的包装规格、已确认定价快照、图片、格式/数量及平台特有规则。已导入 SKU 单独标记 `not_publishable_again`,而不是当作校验失败。
|
||
|
||
响应 `data`:
|
||
|
||
```json
|
||
{
|
||
"ready": false,
|
||
"profile": {
|
||
"ready": false,
|
||
"issues": [
|
||
{
|
||
"code": "OZON_ATTRIBUTE_REQUIRED",
|
||
"field": "attributes.10001.0",
|
||
"message": "请填写最小儿童年龄",
|
||
"platform_attribute_id": "10001"
|
||
}
|
||
]
|
||
},
|
||
"variants": [
|
||
{
|
||
"variant_id": "uuid",
|
||
"display_name": "红色",
|
||
"status": "blocked",
|
||
"ready": false,
|
||
"issues": [
|
||
{
|
||
"code": "OZON_ATTRIBUTE_REQUIRED",
|
||
"field": "variants.uuid.attributes.10096.0",
|
||
"message": "请填写颜色",
|
||
"platform_attribute_id": "10096"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
`profile.issues` 包含商品级基础信息、类目 schema 和 `is_aspect=false` 的必填属性;每个 SKU 的 `issues` 包含货号、价格、有效包装规格、图片和 `is_aspect=true` 的必填属性。只有 `confirmation_status=confirmed` 且包含非空值的属性满足发布要求。`ready=true` 要求 profile 无问题、至少存在一个未上架 SKU,且全部未上架 SKU 均通过校验。
|
||
|
||
`POST /api/product-platform-profiles/{profile_id}/publish-preflights`
|
||
|
||
Body:`{ "variant_ids": ["uuid", "uuid"] }`,最多 100 个且不能重复。该接口是创建真正发布 job 之前的可重算预检,不固化商品发布快照;平台目录任务按资源 `scope_key` 幂等合并,因此重复预检不会并发重复请求 Ozon。
|
||
|
||
预检只接受当前 profile 下 `not_listed` / `failed` 的未删除 SKU。`submitting` 返回 `409 SKU_IMPORT_IN_PROGRESS`,`imported` 返回 `422 SKU_ALREADY_IMPORTED`,不属于当前商品或已删除返回 `422 PUBLISH_VARIANTS_INVALID`。
|
||
|
||
目录处理顺序:
|
||
|
||
1. 只检查当前 `platform_product_category_id` 的 schema。fresh 直接使用;stale 使用旧 schema 并创建/复用 low、`trigger_kind=publish` 的平台任务;expired/missing 创建/复用 high 任务。
|
||
2. schema 可用后,从本次 SKU 集合的已确认草稿中提取实际使用的字典属性。未填写的可选字典、其他 SKU 和同类型下未使用字典都不刷新。
|
||
3. 字典沿用相同 14/21 天状态机。expired/missing 候选值不能作为发布依据;预检最多共用一个 5 秒等待窗口观察所有 high Worker 任务,不按字典数量逐项累加等待。
|
||
4. high schema 成功后,预检再根据新 schema 计算字典依赖;最后重新执行必填、scope、包装、价格、图片以及字典值有效性校验。
|
||
|
||
可立即完成时返回 HTTP `200`;仍在等待 high 任务时返回 HTTP `202`。响应包含 `status=ready|blocked|refreshing`、完整 `validation`、schema/已用字典的 `catalog` cache 元数据,以及本次创建或复用的 `refresh_runs`。
|
||
|
||
`refreshing` 时客户端使用相同 SKU 集合重新预检;不得退回过期缓存。`blocked` 表示目录资源可用但商品草稿仍有业务校验问题;`ready` 才能进入未来的发布快照创建事务。Ozon 最终仍可能拒绝 payload,该错误属于具体商品/门店的发布结果,不能据此创建门店级目录副本。
|
||
|
||
当前预检只对 profile 引用的类目执行本地活动状态校验,并对该类目的 schema 与实际使用字典执行新鲜度刷新;它尚未判断整棵类目树缓存是否过期,也不会因树过期先创建树同步任务。若本地类目不存在、已停用或不属于当前平台,API 返回 Atlas 自己的 `422 CATEGORY_INVALID`,这不是 Ozon 接口响应。后续真正发布 job 接入前,需要明确补齐树级新鲜度保护或确认“每周同步 + 本地活动状态”足以作为发布边界。
|
||
|
||
## 4. SKU 定价确认
|
||
|
||
`POST /api/product-platform-profiles/{profile_id}/variants/{variant_id}/pricing-confirms`
|
||
|
||
Headers:`Idempotency-Key` 必填。Body:
|
||
|
||
```json
|
||
{ "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:
|
||
|
||
```json
|
||
{ "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 首次上架
|
||
|
||
> 当前实现到上述 `publish-preflights`。以下 `publish-jobs`、Ozon `/v3/product/import` 提交和结果轮询仍是冻结契约,尚未开发;不得把预检 `ready` 当成已经提交或发布成功。
|
||
|
||
`POST /api/product-platform-profiles/{profile_id}/publish-jobs`
|
||
|
||
Headers:`Idempotency-Key` 必填。Body:
|
||
|
||
```json
|
||
{ "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、该 SKU 的有效包装规格和平台适配请求,再投递 Worker。适配请求中的每个 Ozon `items[]` 必须包含该 SKU 独立解析出的 `depth`、`width`、`height`、`dimension_unit`、`weight`、`weight_unit`,不得省略或传 0。返回 `202`:
|
||
|
||
```json
|
||
{ "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`](../product/api.md) 中与商品编辑有关的路径。旧草案保留为历史背景,实施以本文件、[`pricing.md`](./pricing.md) 与 [`collection-and-extension.md`](./collection-and-extension.md) 为准。
|