feat(edit): 编辑上报细节优化

This commit is contained in:
2026-10-06 18:30:05 +08:00
parent e20cd7547f
commit 79b310efe7
92 changed files with 2764 additions and 368 deletions
+22 -2
View File
@@ -18,6 +18,8 @@ Headers:`Idempotency-Key` 必填,页头平台由 header 指定,body 为空
- 已存在 profile 时只读取,绝不因再次进入而覆盖用户已选门店、类目、属性或价格;
- 不创建跨平台 profile,也不改变其他平台的上架状态。
插件「编辑上架」打开 `/products/{product_id}/editor?collection_item_id={collection_item_id}` 时,Web 先按页头当前平台调用此接口。核对响应 `product_id` 与路径一致后读取编辑快照,使首次采集和既有草稿均进入对应商品编辑页;采集接口本身不创建编辑上下文。
返回:
```json
@@ -102,6 +104,8 @@ Headers:`Idempotency-Key` 必填,页头平台由 header 指定,body 为空
供应商异步任务返回终态失败时不再轮询或重试同一任务 ID。只有供应商明确返回内容政策或审核错误码时才使用稳定错误码 `AI_IMAGE_CONTENT_REJECTED`;笼统的 `failed`(即使消息称“可能”涉及安全政策)及其他供应商终态失败使用 `AI_IMAGE_PROVIDER_FAILED`;提交、网络、存储等非终态问题在重试耗尽后使用 `AI_IMAGE_GENERATION_FAILED`。Worker 在终态失败或重试耗尽时把失败来源、步骤和脱敏摘要写入同工作空间的 operation 结果;用户界面显示阶段和摘要,并在任务卡片展示已取得的 Pcore 原始错误内容。历史任务若原先未保存错误内容,则相关字段为 `null`。
Pcore 返回的图片地址若与服务端 `PCORE_BASE_URL` 同源,且严格对应当前已保存任务的 `/v1/images/content/{provider_task_id}/0`(无凭据、查询参数和片段),Worker 将其作为供应商任务 API 读取:从配置地址与任务 ID 重新构造 HTTPS 请求,保持 TLS 校验,不发送 API Key,不跟随重定向,下载上限 25 MiB,后续仍校验实际图片格式和尺寸。该受控接口沿用供应商 API 的系统网络解析,可在开发代理 Fake-IP DNS 环境下工作;不将 Fake-IP 地址加入通用下载白名单。外部图片地址继续校验公网 IPv4 并固定连接地址;同源但不匹配当前任务的路径直接拒绝。此处理共用于单张编辑与套图,不改变 HTTP 接口字段或任务状态契约。
生成结果先在单张 AI 编辑抽屉中预览。用户点击“添加到当前 SKU”后,新素材插入原图后方,原图保留;选图弹窗“确认”仅写入编辑页表单,页面“保存”后才持久化 SKU `media_asset_ids`。关闭或取消编辑不修改 SKU 草稿。图片源、参考图、结果均受工作空间隔离;任务输出与源图分别存储。
`PUT /api/products/{product_id}/editor-draft`
@@ -141,14 +145,15 @@ Headers:`If-Match: "{product_revision}:{profile_revision}"`、`Idempotency-Key
- 保存来源素材不会修改原始采集证据。`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` 开始。
- `variants` 是当前完整未删除 SKU 集合,最多 1000 项(2026-10-06 从 100 提高,与采集载荷共享 `PRODUCT_VARIANT_LIMIT`)。省略既有 SKU 表示软删除;无论当前 SPU 状态都允许新增、复制、删除。`copy_from_variant_id` 仅用于新行,复制通用 SKU 数据但不复制任何平台导入映射,生成的新 SKU 从 `not_listed` 开始。超出上限返回字段校验错误且不部分保存;首次发布每批最多 50 个 SKU 的独立限制保持不变。
- `variants[].purchase_cost_cny` 是人民币采购价,编辑快照以两位小数字符串返回;草稿输入最多两位小数。来源采集写入时按两位小数四舍五入。
- 删除已导入 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 当前平台的最终选图顺序;最多 30 张。格式只允许 JPG/PNG/WebP,单文件最多 20 MB,边长超过 10,000 px 拒绝,短边小于 700 px 不得作为首图并给出警告。第一张是主图。已完成的 `product_image` 上传素材和网络导入素材可供同工作空间的 SKU 选择;当前商品成功的水印与 3:4 处理结果也可选择。采集图片仍须先转存,且来源变体须匹配。
- `media_asset_ids` 是本 SKU 当前平台的最终选图顺序;最多 30 张。格式只允许 JPG/PNG/WebP,单文件最多 20 MB,边长超过 10,000 px 拒绝,短边小于 700 px 不得作为首图并给出警告。第一张是主图。已完成的 `product_image` 上传素材和网络导入素材仅可供所属商品的 SKU 选择;当前商品成功的水印与 3:4 处理结果也可选择。采集图片仍须先转存,且来源变体须匹配。
- `common.model` 是用户手填或主动 AI 填报的商品型号;采集时允许为空,但每次首次上架前必须非空。`offer_id` 是用户手填或主动 AI 填报的目标平台卖家货号;采集入箱时不填值,生成时不直接复用 `source_sku`。草稿可为空,但每个待首次上架 SKU 的 `offer_id` 必须非空且在同一店铺/平台唯一。AI 填报接口和回填规则见[方案](../product/ai-autofill-plan.md)。
- 商品编辑器的货号控件固定展示 `common.model + "-"` 前缀,用户只编辑 SKU 后缀;请求中的 `offer_id` 仍为完整货号,无新增字段。未导入 SKU 修改型号时保留后缀、同步前缀;后缀为空时货号仍为空。旧货号匹配当前型号前缀时拆出后缀,否则将完整旧值保留为后缀,补前缀只进入待保存草稿,不批量改写历史数据。AI 回填遵循相同组合方式。完整货号继续限制 50 字符,导入/进行中 SKU 保留原货号并只读,已有型号锁定规则继续生效。
- `common.source_title` 在编辑页显示为“中文商品名”;`profile.title` 显示为“俄文标题”。`profile.title_mode` 为 `common` / `variant`,编辑快照必须返回当前模式,保存草稿时可传入;旧客户端省略时保留原模式,新商品默认为 `common`。切到 `variant` 时清空并禁用 SPU 俄文标题,在各 SKU 展示标题输入;切回 `common` 时经确认清空所有 SKU 俄文标题,恢复 SPU 输入。两种模式均允许空白草稿;首次上架预检在 `common` 模式检查 `profile.title`,在 `variant` 模式逐个检查待发布 SKU 的 `variants[].title`,缺失时返回 `PUBLISH_TITLE_REQUIRED`。草稿仍须携带有效的 `target_store_id` 作为店铺上下文,并遵守已填写字段的格式和长度约束。SKU 标题 `variants[].title` 为 `null` 时在旧覆盖语义下继承 `profile.title`,不回退到 `display_name`;新版逐 SKU 模式不允许发布时依赖该继承。现有 SKU `tags=[]`、`description=null` 保持继承语义。标题、标签、描述分别独立覆盖。中俄对照按 SPU/SKU 与字段随 `editor-draft` 原子保存;SKU 继承项直接引用 SPU 对照,手工俄文覆盖但无匹配中文时标为对照待更新。发布适配器使用 SKU 有效文案仍是后续工作。
成功返回新 `product.revision`、`profile.revision`、创建的 SKU ID/`client_key` 映射和 warnings。任一字段、属性或媒体无效时返回 `422` 且不部分保存;版本冲突返回 `409 VERSION_CONFLICT`,客户端应重新读取编辑快照。
@@ -175,6 +180,8 @@ Headers:`If-Match`、`Idempotency-Key` 必填。Body:`{ "target_store_id": "
当前 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。
商品颜色按 Ozon 属性 ID `10096` 识别,允许每个 SKU 选择最多 100 个有效字典值。此业务规则统一用于编辑、草稿保存和发布快照,优先于目录中 `is_collection=false` / `max_value_count=1` 的单值标记;其他属性继续遵守自身 schema。草稿仍保存字典值 ID,发布时逐项映射为 `{ dictionary_value_id, value }`,不得只保留第一个颜色或合并成一条文字值。Web 已选标签保留对应名称,初始化时加载颜色字典;名称加载前可使用已知颜色的俄文名称兜底,编号不作为颜色文本显示。
`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}`
@@ -259,6 +266,8 @@ Body:`{ "variant_ids": ["uuid", "uuid"] }`,最多 50 个且不能重复。
服务端核对预览归属、15 分钟有效期、当前采购价、有效包装、类目、门店、币种及规则版本,再写入不可变 `pricing_quotes`、全部候选和 listing 价格。任何一个明确选择的报价失效,整个商品保存回滚。客户端可提示用户重新计算,或经用户选择清除本次确认意图后仅保存草稿。
已上架(`imported`)SKU 可通过「重新试算」或「试算详情 → 应用到 SKU」显式选择新报价。应用前提示「该 SKU 已上架。本次价格调整仅保存为本地记录,不会同步至 Ozon;如需修改线上价格,请前往 Ozon 后台。」取消不应用报价,确认后仍须保存商品。编辑快照 `variants[].listing.store_id` 返回首次导入店铺,已上架 SKU 按此店铺试算和保存,不能改用 profile 后来选择的目标店铺。服务端仅在携带 `pricing_selection` 时更新其本地报价引用和价格;普通草稿保存保留原报价。首次导入状态、店铺、货号、平台商品 ID 和导入时间不变,不创建发布任务或价格同步任务。
没有报价时仍可保存商品,但首次发布预检返回 `PRICING_REQUIRED`。已确认报价不会随预览到期而失效;采购、包装、目标门店/类目、履约或门店币种改变后须重新确认。调整默认方案、汇率或费率不会反向改写历史快照。详见[定价 API](./pricing.md)。
经维护者 2026-09-29 确认,使用草稿物流费率卡完成的报价可以保留到首次发布预检;未审核状态只作为来源信息展示,不再生成 `PRICING_RATE_REVIEW_REQUIRED`。当前仍要求 SKU 已确认有效报价、售价、包装和其他发布必填数据。生产环境默认可使用草稿卡,管理员可通过 `PRICING_ALLOW_DRAFT_RATES=false` 限制其进入新试算;审核流程仍可用于核实费率。
@@ -288,11 +297,20 @@ AI 填报接口只创建异步任务并返回 `202` + `ai_task_id`,不直接
| 接口 | body 核心字段 | 成功结果 |
|---|---|---|
| `POST /api/product-platform-profiles/{profile_id}/ai/autofills` | 可选 `hint` 与策略开关;Header 必填 `Idempotency-Key` | 一次创建类目、型号、未导入 SKU 货号、可证包装规格及中俄文案的填报任务。Ozon 来源先运行确定性类目匹配;其他来源使用受限类目树候选。 |
| `POST /api/product-platform-profiles/{profile_id}/variants/{variant_id}/ai/copy` | 可选 `hint`(用户描述/生成要求,去除首尾空白后最多 1000 字符);Header 必填 `Idempotency-Key` | 为指定 SKU 创建中俄文案任务,固化用户要求与商品输入;返回 `202`、`ai_task_id` 和 `queued`。 |
| `POST /api/products/{product_id}/ai/suite-plans` | `variant_id`、底图、套图资料与配置 | 创建显式绑定一个目标 SKU 的异步逐张规划;生成由 `POST /api/products/{product_id}/ai/image-suites` 提交。详见 [套图契约](./product-image-suites.md)。 |
| `POST /api/product-platform-profiles/{profile_id}/variants/{variant_id}/ai/dimension-images` | 可选样式 | 固化该 SKU 合并后的**实物**规格快照;实物规格缺失/待确认返回 `422`。 |
`GET /api/product-ai-tasks/{ai_task_id}` 返回任务状态、输入版本、脱敏结果或错误;全局填报成功时返回类目或候选、型号、逐 SKU 货号、可证包装建议及 SPU 中俄文案,不生成 SKU 文案。单 SKU 文案由 `POST /api/product-platform-profiles/{profile_id}/variants/{variant_id}/ai/copy` 单独创建任务,在弹窗预览并由用户应用。Ozon 动态类目属性不属于当前 AI 填报范围,仍通过类目确认后加载的 schema 由用户填写。歧义类目仍需用户选择。中文对照与俄文在下一次 `editor-draft` 中一同保存,读取编辑上下文时一同返回。
单 SKU 文案中的 `hint` 是本次用户直接提供的描述、事实修正与生成要求,优先于采集标题、描述、旧 SKU 资料及已有文案;发生冲突时使用用户明确的修正,未修正的事实从当前 SKU 与来源资料补充。采集资料中的命令只当作资料,不可覆盖用户要求。JSON、中俄配对、俄文发布字段、标题及标签格式等输出契约继续生效。该要求只固化在本次 AI 任务输入中,不自动改写采集信息或商品草稿。修改要求会使弹窗旧结果失效,须重新生成后才能应用;关闭或切换 SKU 后,旧异步结果不能回填新弹窗。
应用当前 SKU 的标题结果而需从通用模式切换为逐 SKU 时,其他 SKU 原先继承的通用标题及对应中文对照转为各自的草稿值,继续保留其有效内容;仅当前 SKU 使用新生成结果。用户手工切换标题模式的原有清空规则不变。
### 2026-10-05 货源链接来源回填
编辑快照 `product.common.purchase_url` 优先返回商品手工保存的非空链接;为空时返回关联采集记录的 `source_url`,手动创建且无来源时仍为 `null`。插件已上报 `source.canonicalUrl`,新采集商品据此初始化货源链接;历史商品只在读模型中回退,不批量修改草稿或 revision。手工非空链接不因重复采集覆盖;保存 `null` 表示恢复使用采集来源地址。请求/响应字段及链接格式校验不变。
## 7. SKU 首次上架(2026-09-30 实施修订)
底部提供「上架至 OZON」和「部分上架」。前者选择全部 `not_listed` / `failed` 活动 SKU,列出已导入或正在提交的跳过原因;后者由用户选择相同资格的 SKU。一次 1–50 个,超过时提示分批,不自动拆批。先保存草稿,再对相同选择集合预检,用户确认后创建任务。
@@ -315,6 +333,8 @@ Headers:`X-Atlas-Platform-Id`、`Idempotency-Key`、`If-Match: "product_revisi
每个数组项表示一个标签,必须以单个 `#` 开头,正文只允许 Unicode 字母、数字和下划线,标签内不得包含空格或其他 `#`;每项最多 30 个字符(含 `#`)。Web 对手工输入显示具体格式提示,草稿仍可保存供后续修改;发布预检及创建任务按所选 SKU 的有效标签(SKU 覆盖或 SPU 继承)检查,不合规时返回 `OZON_HASHTAG_INVALID` 字段问题并阻止发布。未被选中或被 SKU 覆盖的通用标签不阻断当前发布。AI 全局填报和单 SKU 文案提示词采用同一格式要求;输出回填前补齐缺失的 `#`,将词间空白、连字符转为下划线,过滤仍含非法字符、重复或超长的标签,并保留对应中文对照。不自动改写手工及历史草稿标签。
产品描述/简介的编辑、草稿及发布快照保留纯文本中的内部换行和空行。Worker 生成 Ozon 请求时,仅将简介属性 `4191` 的 CRLF/CR 规范为 LF 并转换成 `<br>`,连续换行转换成连续 `<br>`;文本中的 `&`、`<`、`>` 转义为实体,避免被当成 HTML 标签。其他属性不转换,快照不被修改。简介换行标签依据 [Ozon 官方商品描述规范](https://global-help.ozon.com/zh/products/requirements/product-info/product-description)。Web 通用/SKU 描述及文案预览的复制入口直接复制原始纯文本,保留换行和空行,不复制上报用的 HTML;SKU 空描述复制其有效的 SPU 继承文本。此修复影响后续提交,不自动更新已导入的 Ozon 商品。
```json
{ "data": { "publish_job": { "id": "uuid", "profile_id": "uuid", "store_id": "uuid", "status": "queued", "item_count": 2, "created_at": "ISO8601", "completed_at": null, "items": [] } } }
```