feat(edit): 商品编辑页开发
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ API 启动后会在 `/api-docs` 提供 Swagger UI,并在其 OpenAPI 文档中
|
||||
| [`watermarks-and-assets.md`](./watermarks-and-assets.md) | 部分已实现 | 水印模板、字体、店铺推荐水印,以及水印图片直传、校验和短时预览;图片渲染派生待实现。 |
|
||||
| [`pricing.md`](./pricing.md) | 已设计 | 汇率、物流价卡、即时试算、定价方案、报价快照衔接。 |
|
||||
| [`collection-and-extension.md`](./collection-and-extension.md) | 已设计 | 插件授权、采集写入、采集箱查询、素材转存与状态。 |
|
||||
| [`product-editor-and-publishing.md`](./product-editor-and-publishing.md) | M3.1 部分已实现 | 编辑上下文、只读快照和产品/平台文案/SKU 的原子草稿保存;类目/属性、媒体、定价、AI 与首次上架仍待实现。 |
|
||||
| [`product-editor-and-publishing.md`](./product-editor-and-publishing.md) | M3.5 部分已实现 | 编辑上下文、完整 SKU 草稿、店铺切换、规格继承、AI 来源素材字段级合并,以及 Ozon 类目树、动态属性和关键词字典缓存同步;媒体、定价、AI 任务与首次上架仍待实现。 |
|
||||
| [`online-listings.md`](./online-listings.md) | 初版已设计 | 店铺独有的在线商品镜像、读取与异步同步;编辑/认领仍待讨论。 |
|
||||
|
||||
旧的 [`../product/api.md`](../product/api.md) 是商品域的早期草案:已确认的产品规则可作为背景,但不能当作最终 HTTP 契约。后续商品模块会迁入本目录并替换其中的路径和细节。
|
||||
|
||||
@@ -26,7 +26,9 @@ Headers:扩展 Bearer token 与 `Idempotency-Key` 必填。请求体为已定
|
||||
|
||||
请求体不得包含或传入 `workspace_id`、`target_platform_id`、`platform_id`、`store_id`、`api_key`、Cookie、授权头或来源站会话字段。服务端从扩展令牌解析工作空间,并将来源 `platform` 映射为系统已启用的来源平台。
|
||||
|
||||
服务端在一个事务中:检查工作空间采集箱上限 → 按 `workspace + source_platform + source_item_key` 查找活动记录 → 创建记录,或以本次页面快照覆盖已有记录的来源 URL、标题、价格、原始证据、规范化载荷、SKU 草稿与候选素材。采集阶段的图片和视频只保存为 `pending_selection` 候选记录,**不下载媒体、不投递素材 Worker**;用户在编辑流程中选择媒体后,才调用按需转存接口。重复上报不会增加采集箱配额,且会保留原有 `collection_item` 与 `product` ID;旧快照中尚未完成的候选或任务会被取消,新快照使用新的来源快照版本,旧 Worker 不得回写新快照。
|
||||
服务端在一个事务中:检查工作空间采集箱上限 → 按 `workspace + source_platform + source_item_key` 查找活动记录 → 创建记录,或以本次页面快照覆盖已有记录的来源 URL、标题、价格、原始证据、规范化载荷与候选素材。采集阶段的图片和视频只保存为 `pending_selection` 候选记录,**不下载媒体、不投递素材 Worker**;用户在编辑流程中选择媒体后,才调用按需转存接口。重复上报不会增加采集箱配额,且会保留原有 `collection_item` 与 `product` ID;旧快照中尚未完成的候选或任务会被取消,新快照使用新的来源快照版本,旧 Worker 不得回写新快照。
|
||||
|
||||
尚未创建任何 `product_platform_profile` 时,SKU 草稿仍是来源投影:同一 `source_variant_key` 原位更新,新 key 创建,缺失 key 软删除。**一旦进入任一平台编辑器,SKU 草稿及其平台 mapping 不再是可替换来源投影。** 此后的重复采集仍更新来源证据、规范化载荷和候选素材;同 key 复用原 SKU ID 且不覆盖用户编辑字段,新发现的来源 SKU 会为每个已有平台配置创建 `not_listed` mapping,来源未返回的 SKU 不自动删除或恢复。来源差异的人工审查 UI 另由商品编辑模块提供,采集接口不得通过删除 SKU 或级联删除映射来“覆盖”草稿。
|
||||
|
||||
成功返回 `201`(新建)或 `200`(复用):
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 商品编辑与首次上架 API
|
||||
|
||||
> M3.0 已实现第 1 节的 `POST /editor-contexts` 和 `GET /editor` 基础读模型,以及对应数据库归属约束。M3.1 已实现第 2 节的产品基础字段、当前平台文案、SKU 与 `offer_id` 的原子草稿保存及双版本并发控制;规格、类目/属性切换、媒体选择、定价、AI 与发布仍是冻结契约,尚未实现。
|
||||
> 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 已通过 Worker 同步 Ozon 类目树,并按类目/关键词按需同步 schema 和字典缓存;媒体选择、定价、AI 任务与发布仍是冻结契约,尚未实现。
|
||||
|
||||
> 此处的商品始终是采集箱内的 Atlas 草稿。所有平台范围接口要求 `X-Atlas-Platform-Id`。商品通用资料可以跨平台复用;`product_platform_profile`、SKU 首次导入状态和在线商品不能跨平台/门店复用。
|
||||
|
||||
@@ -19,6 +19,7 @@ Headers:`Idempotency-Key` 必填,页头平台由 header 指定,body 为空
|
||||
|
||||
```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"
|
||||
@@ -41,15 +42,37 @@ Headers:`Idempotency-Key` 必填,页头平台由 header 指定,body 为空
|
||||
"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" },
|
||||
"category": { "id": "uuid", "platform_type_id": "123", "source": "source_capture", "name": "收纳箱", "path": "家居 › 收纳 › 收纳箱" },
|
||||
"listing_summary": { "status": "partially_listed", "detail": "1 个 SKU 已导入" }
|
||||
},
|
||||
"variants": [], "attributes": [], "media_candidates": [], "attribute_schema": null,
|
||||
"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`、价格、首次导入状态、最近脱敏失败原因)及已选媒体。`attribute_schema` 仅在当前类目已确定时返回。候选和已选媒体只含 `asset_id`、尺寸、MIME、来源角色与短时预览 URL;不含 `object_key` 或永久 URL。
|
||||
`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 规则
|
||||
|
||||
@@ -59,11 +82,17 @@ 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", "platform_type_id": "123", "category_source": "manual" },
|
||||
"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",
|
||||
@@ -78,6 +107,11 @@ Headers:`If-Match: "{product_revision}:{profile_revision}"`、`Idempotency-Key
|
||||
|
||||
语义与校验:
|
||||
|
||||
- `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。`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` 时,不允许定价确认或首次上架,但仍允许保存草稿。实物规格只影响尺寸图,不用于物流。
|
||||
@@ -96,11 +130,27 @@ Headers:`If-Match`、`Idempotency-Key` 必填。Body:`{ "target_store_id": "
|
||||
|
||||
`GET /api/product-platform-profiles/{profile_id}/attribute-schema`
|
||||
|
||||
当前 profile 必须已有类目。返回平台适配后的字段 schema、字典端点、`is_required`、`is_collection`、`max_value_count`、`is_aspect` 和适用 scope。类别未选返回 `422 CATEGORY_REQUIRED`。
|
||||
当前 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}`
|
||||
|
||||
转发并缓存平台字典值;不让前端直接持有 Ozon 密钥。字典缺失、过期或类目/属性不匹配时返回 `422`。
|
||||
返回本地同步的字典值 `{ 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}`
|
||||
|
||||
返回当前平台最多 100 个活动类目,包含本地 UUID、平台外部类目/类型 ID、名称与路径。选择类目本身不立即写库,随下一次 `editor-draft` 原子保存。
|
||||
|
||||
`POST /api/product-platform-profiles/{profile_id}/category-syncs`
|
||||
|
||||
请求目标门店的已验证 Seller API 凭据同步 Ozon 类目树,返回 `202` 与 `platform_category_tree_sync` operation。Worker 将树写入全局平台缓存;密钥仅在 Worker 内存中解密,outbox 与响应均不包含密钥。
|
||||
|
||||
`POST /api/product-platform-profiles/{profile_id}/attribute-schema-syncs?category_id={uuid}`
|
||||
|
||||
为已选活动类目创建 `platform_category_attribute_sync` operation。同步以单类目事务替换 schema;下一次编辑草稿保存会依最新 schema 校验并将不匹配旧值转为未映射草稿。
|
||||
|
||||
`POST /api/product-platform-profiles/{profile_id}/attribute-dictionaries/{dictionary_id}/syncs?category_id={uuid}&q={keyword}`
|
||||
|
||||
按关键词(至少两个字符)创建 `platform_attribute_dictionary_sync` operation,Worker 使用 Ozon 字典搜索接口并 upsert 命中的本地值。不会为打开一个字段而全量下载大字典;前端先读本地缓存,缺少结果时由用户明确选择“从 Ozon 搜索”。
|
||||
|
||||
`GET /api/product-platform-profiles/{profile_id}/validation`
|
||||
|
||||
@@ -140,6 +190,8 @@ Headers:`Idempotency-Key` 必填。Body:
|
||||
|
||||
所有 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 来源默认不自动调用,但用户可主动调用。 |
|
||||
|
||||
Reference in New Issue
Block a user