feat(edit): 定价试算开发、SKU 样式功能修改

This commit is contained in:
R524809
2026-09-29 17:50:19 +08:00
parent 84b230e69a
commit 0a303414d8
52 changed files with 4278 additions and 1393 deletions
+24
View File
@@ -0,0 +1,24 @@
# 类目佣金导入 API
> 2026-09-29。产品方案见 `docs/product/category-commission-import-plan.md`。所有金额与费率为十进制字符串;导入仅支持 Ozon / RU / rFBS / RUB。
## 管理端
全部 `/system/pricing/commissions` 接口要求实时有效的系统管理员授权,普通用户返回 403 `SYSTEM_ADMIN_REQUIRED`。
- `GET /system/pricing/commissions?platform_id={uuid}`:返回导入版本列表(不含原始文件),最近版本在前。
- `GET /system/pricing/commissions/:id`:版本详情,含解析后的类目、档位、异常、变更状态。
- `POST /system/pricing/commissions/preview`:只读预览。请求 `{platform_id,version,source_reference,captured_on,document}`,`document` 为来源 JSON 对象。返回 `{preview_hash,categories,summary,previous_import_id}`。分类结果为 eligible/excluded,差异为 added/changed/unchanged;summary 包含总类目、规则数、可用/排除类目、映射商品类型、移除类目数。格式不正确返回 422 `COMMISSION_IMPORT_INVALID`;允许预览全部无法映射的结果;没有可映射类目时禁止启用空版本。
- `POST /system/pricing/commissions/imports`:201,要求 UUID `Idempotency-Key`。请求在预览输入基础上增加 `{preview_hash,confirm_scope:true}`。确认适用范围表示管理员确认数据适用于 Ozon RU/rFBS 的 RUB 售价;目录异常和移除数量只在预览中提示,不要求额外勾选确认。只支持立即启用,完整替换本入口的前一版本,不修改非本入口规则;发生其他来源有效映射冲突时拒绝启用。
预览指纹覆盖完整输入、解析结果、可映射的商品类型及当前版本。启用时锁平台并重新核对,变化返回 409 `COMMISSION_PREVIEW_STALE`。同一来源内容不重复导入,同平台版本名称唯一,冲突返回 409 `COMMISSION_IMPORT_EXISTS`。旧导入的规则/映射结束有效期,新规则使用新行;禁止改写历史费率,历史报价保持不变。
最多 100 个一级类目、2,000 个二级类目,JSON 总大小最多 2 MiB。首版只接收三种固定区间的完整组合,校验叶子序号及 label/value 百分比一致、所有来源 cate_id 唯一;0% 是有效佣金,可正常导入;未匹配、父子关系不符、无有效类型的类目在预览中提示,并因无法建立商品类型映射而暂不参与算价。缺少或重叠档位不能发布。
## 卖家查询
`GET /pricing/commission-rules?store_id={uuid}&platform_product_category_id={uuid}`,也可用 `description_category_id={外部类目ID}` 替代内部类目 ID,两者必须且只能指定一个。当前固定 RU/rFBS,无需前端重复传入履约枚举。
需要门店属于当前工作空间。返回 `{data:{category_name,currency_code,bands,version,source,effective_from}}`;每档含 `{id,min_price,max_price,min_inclusive,max_inclusive,commission_rate}`。内部类目必须属于门店平台且有效;外部 ID 必须能唯一解析到同平台已核对佣金类目。没有可用规则返回 422 `PRICING_COMMISSION_UNAVAILABLE`,不能将缺失规则推定为 0% 佣金。
报价与此查询共用正式佣金解析器。现有 `PricingOption` 增加可选 `commission_details`(类目、区间、来源版本),以兼容已持久化的历史报价。规则换版会使待确认预览失效,已确认报价不重新计算。
+7 -7
View File
@@ -10,8 +10,8 @@
需要已认证工作空间。返回 `{data: PricingCatalog}`:
- `store`:门店 ID、名称及报价币种(CNY / RUB)。
- `rate_cards[]`:该门店平台、RU / rFBS 的有效费率卡及运输方式。正式环境必须 active、在有效期内、有审核时间及审核人。`development_only=true` 表示本地开发模式临时可选的 draft 卡。
- `pending_rate_cards[]`:该平台 RU / rFBS 待审核草稿的名称、版本和状态,仅用于说明阻断原因,不作为方案或报价可选项。
- `rate_cards[]`:该门店平台、RU / rFBS 的可用费率卡及运输方式,包含已审核且在有效期内的 active 卡,以及配置允许时的 draft 卡。`development_only=true` 为兼容现有 DTO 保留,表示所选卡尚未审核,并不代表禁止首次上架。
- `pending_rate_cards[]`:该平台 RU / rFBS 待审核草稿的名称、版本和状态;在允许草稿报价时也可能同时出现在 `rate_cards[]`,用于展示审核状态。
- `policies[]`:该门店活动方案,默认方案优先,包含完整参数和 revision。
- `exchange_rate`:CBR 最新 CNY→RUB 官方参考汇率,包含 ID、日期和 stale;缺少数据时为 null。超过 72 小时提示过期参考,不自动代填其他来源。
@@ -46,7 +46,7 @@
```
- 独立试算 `binding=null`;SKU 必须提供 profile,已有 variant_id 与新建 client_key 二选一。验证资源归属,已上架 SKU 不接受新报价。
- 包装输入是编辑草稿合并后的有效 g/mm 值,必须已确认;尺寸最多三位小数,重量至少 1 g。保存时从实际持久化草稿重新合并并核对,不能仅信任客户端摘要。
- 包装输入是编辑草稿合并后的有效 g/mm 值,必须已确认;接口为兼容历史值允许最多三位小数,重量至少 1 g,当前编辑页与试算界面只接受正整数。保存时从实际持久化草稿重新合并并核对,不能仅信任客户端摘要。
- `mode=target_markup`:目标利润=采购成本×加价率;求解销售价。`fixed_sale`:selling_price 必填,按该售价核算利润。
- 百分比费用以销售额为基数;除加价率外每项小于 1、最多六位小数。引擎拒绝总扣费率≥1。划线价=销售价÷(1−折扣率)。
- 枚举运输方式×产品类型×佣金档,按最终币种舍入后回验货值、佣金、重量和体积重边界。默认运输不可用时 recommended_option_id=null,不替换为另一方式。
@@ -93,13 +93,13 @@ context 保存规则版本、实际汇率与来源,以及系统汇率记录 ID
| DELETE | `/pricing-policies/{id}` | Idempotency-Key;归档并清除默认标志,返回 `{id,archived:true}` |
写入 `{name,note,parameters,allow_commission_override}`。只保存费用、加价、折扣、固定/系统汇率与运输偏好;采购价、包装、手动售价及临时佣金不属于方案持久字段。
方案的默认和比较运输必须属于同一可用费率卡;正式环境仅允许已审核卡。历史快照不会被方案修改;尚未确认的预览因方案版本改变要求重算。
方案的默认和比较运输必须属于同一可用费率卡;正式环境默认允许有效草稿卡;可通过 `PRICING_ALLOW_DRAFT_RATES=false` 仅使用已审核卡。历史快照不会被方案修改;尚未确认的预览因方案版本改变要求重算。
## 5. 数据启用与错误
迁移中的 CEL 目录为 `draft`,覆盖六种产品类型、三种运输方式,香港空运不纳入。真实账单复核、有效日期/审核人以及平台类目佣金映射仍是正式使用前置条件,接口不会回退到示例费率或固定佣金。
迁移中的 CEL 目录为 `draft`,覆盖六种产品类型、三种运输方式,香港空运不纳入。平台类目佣金映射仍是正式试算所需数据;草稿物流费率可按上述配置参与报价,页面保留费率来源标识,接口不会回退到未选定的固定佣金。物流费率审核用于形成有证据的已审核版本,不再是草稿报价上架预检的前置条件。
商品编辑联调期间,API 在 `NODE_ENV=development` 时默认允许 draft 费率卡参与方案保存、预览与草稿报价确认;可设 `PRICING_ALLOW_DRAFT_RATES=false` 关闭。测试环境只有显式设 `PRICING_ALLOW_DRAFT_RATES=true` 才开启;生产环境始终关闭。该模式不修改费率卡状态、不生成审核记录。目录的 `rate_cards[].development_only` 和预览的 `context.development_only` 标识试算性质;基于此类预览确认的 SKU 报价可在编辑页查看,但首次上架校验必须返回 `PRICING_RATE_REVIEW_REQUIRED`,直至使用已审核费率重新试算确认。佣金和汇率仍按现有输入规则处理。
API 在开发与生产环境默认允许 draft 费率卡参与方案保存、预览和 SKU 报价确认;`PRICING_ALLOW_DRAFT_RATES=false` 可显式关闭。测试环境默认关闭,仅显式设为 `true` 才开启。使用草稿卡不会修改费率卡状态或生成审核记录;目录的 `rate_cards[].development_only` 和预览的 `context.development_only` 保留来源标识,供页面和报价快照展示。已确认的草稿卡报价满足定价确认要求,首次上架预检不再因卡片未审核返回 `PRICING_RATE_REVIEW_REQUIRED`;其他报价输入、利润和发布字段校验照常执行。佣金和汇率仍按现有输入规则处理。此变更由维护者于 2026-09-29 要求,影响 Web、API、现有响应说明和发布预检,不改变审核端点。
### 系统管理员审核 CEL 费率
@@ -121,7 +121,7 @@ context 保存规则版本、实际汇率与来源,以及系统汇率记录 ID
|---|---|
| 422 VALIDATION_ERROR | 修正输入格式或缺项 |
| 422 PRICING_RULES_UNAVAILABLE | 配置并审核物流费率 |
| 422 PRICING_COMMISSION_UNAVAILABLE | 配置正式映射,或使用允许临时佣金的方案 |
| 422 PRICING_COMMISSION_UNAVAILABLE | 配置正式映射;缺少正式规则时,可在允许临时佣金的方案或本次自定义参数中填写已核实的临时佣金率 |
| 422 PRICING_FX_UNAVAILABLE | 等待 CBR 同步,或明确采用临时/方案固定汇率 |
| 409 PRICING_PREVIEW_STALE | 重算、比较并重新应用,或显式仅保存草稿 |
| 409 VERSION_CONFLICT | 重新读取版本后修改 |
+11 -4
View File
@@ -13,6 +13,7 @@
Headers:`Idempotency-Key` 必填,页头平台由 header 指定,body 为空。服务端验证采集记录归属、平台可用,并取得或创建该商品在当前平台的 `product_platform_profile`:
- 首次创建时使用该平台默认店铺作为 `target_store_id`,并为所有未删除 SKU 建立本平台 `product_variant_platform_listing`;
- 首期新建 Ozon profile 的履约方式为 `rfbs`,与当前定价费率卡及报价接口一致;其他平台保持 `unknown`,由后续平台适配确定;
- 当前平台没有默认店铺时返回 `422 DEFAULT_STORE_REQUIRED`,只给出店铺管理引导,不创建缺少目标门店的 profile;
- 已存在 profile 时只读取,绝不因再次进入而覆盖用户已选门店、类目、属性或价格;
- 不创建跨平台 profile,也不改变其他平台的上架状态。
@@ -38,7 +39,8 @@ Headers:`Idempotency-Key` 必填,页头平台由 header 指定,body 为空
"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" }
"default_package_spec": { "length_mm": null, "width_mm": null, "height_mm": null, "shipping_weight_g": null, "status": "missing" },
"package_weight_mode": "common", "package_dimensions_mode": "common"
},
"profile": {
"id": "uuid", "revision": 8, "platform_id": "uuid", "market_code": "RU", "fulfillment": "rfbs",
@@ -78,6 +80,10 @@ Headers:`Idempotency-Key` 必填,页头平台由 header 指定,body 为空
`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` 决定。
编辑快照的 `product.package_weight_mode` 和 `product.package_dimensions_mode` 分别保存包装重量、包装尺寸的编辑模式(`common` / `variant`);保存草稿的 `common` 中可传同名字段,旧客户端省略时保留当前模式,新商品默认 `common`。模式独立于数值保存,使逐 SKU 表单尚未填写时重新打开仍保持原模式。切到 `variant` 时客户端清空并禁用对应 SPU 输入;切回 `common` 时经确认清空所有 SKU 对应覆盖值并恢复 SPU 输入。两项切换互不影响,模式不改变有效包装规格的“SKU 覆盖优先、SPU 回退”解析规则。
商品编辑页的包装重量和长宽高仅允许手工输入正整数(g / mm),定价详情弹窗采用相同输入规则。已有小数值保留原样供人工核对,不自动取整;API 暂保留三位小数的历史数据兼容能力。
`source_material` 是供页面展示和 AI 使用的**有效来源素材**,由当前 `collection_items.normalized_payload` 的最新规范化值与用户明确修改过的顶层字段合并而成。未修改字段随重复采集自动更新;已修改字段继续保留用户值。`source_material_state.latest` 保留本次最新规范化来源,`overridden_fields` 表示用户覆盖字段,`changed_fields` 表示复采后与用户稿不同的覆盖字段,页面可据此提示、逐项恢复或全部采用最新采集值。原始 `normalized_payload` 与 `raw_evidence` 始终作为采集证据保留,不因编辑而改变。
`captured_category_match` 仅对 Ozon 来源返回自动类目判定结果。服务端只使用公开详情页的**一级**面包屑和结构化 `Type_0` 商品类型,在当前平台目录的活动可发布叶子中精确匹配;公开页后二、三级导航不参与判定。`matched` 时服务端已自动写入 `source_capture`;`ambiguous` 时前端展示 `candidates` 的完整 Atlas 路径,用户确认后按普通手选类目保存为 `manual`;`unmatched`、`unavailable` 均不得自动选择。
@@ -112,7 +118,8 @@ Headers:`If-Match: "{product_revision}:{profile_revision}"`、`Idempotency-Key
"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" } },
"default_package_spec": { "length_mm": "120", "width_mm": "60", "height_mm": "30", "shipping_weight_g": "400", "status": "confirmed" },
"package_weight_mode": "common", "package_dimensions_mode": "common" },
"profile": { "target_store_id": "uuid", "fulfillment": "rfbs", "title": "Название", "description": "…", "tags": ["tag"],
"platform_product_category_id": "uuid", "category_source": "manual" },
"variants": [{
@@ -142,7 +149,7 @@ Headers:`If-Match: "{product_revision}:{profile_revision}"`、`Idempotency-Key
- 类目变化后不匹配的旧属性作为未映射草稿返回 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 处理结果也可选择。采集图片仍须先转存,且来源变体须匹配。
- `common.model` 是用户手填或主动 AI 填报的商品型号;采集时允许为空,但每次首次上架前必须非空。`offer_id` 是用户手填或主动 AI 填报的目标平台卖家货号;采集入箱时不填值,生成时不直接复用 `source_sku`。草稿可为空,但每个待首次上架 SKU 的 `offer_id` 必须非空且在同一店铺/平台唯一。AI 填报接口和回填规则见[方案](../product/ai-autofill-plan.md)。
- `common.source_title` 在编辑页显示为“中文商品名”;`profile.title` 显示为“俄文发布标题”,页面保存时必填。为允许先保存采集草稿再使用 AI 填报,API 草稿契约仍接受空标题;发布就绪校验在 `profile.title` 为空时返回 `PUBLISH_TITLE_REQUIRED`。SKU 发布标题 `variants[].title` 为 `null` 时继承 `profile.title`,不回退到 `display_name`;现有 SKU `tags=[]`、`description=null` 保持继承语义。标题、标签、描述分别独立覆盖。中俄对照按 SPU/SKU 与字段随 `editor-draft` 原子保存;SKU 继承项直接引用 SPU 对照,手工俄文覆盖但无匹配中文时标为对照待更新。发布适配器使用 SKU 有效文案仍是后续工作。
- `common.source_title` 在编辑页显示为“中文商品名”;`profile.title` 显示为“俄文发布标题”。编辑页保存草稿时允许中文商品名、俄文发布标题、型号、品牌、产品类目及类目必填属性留空,必填标记只提示首次上架所需资料;首次上架预检统一检查缺失项,`profile.title` 为空时返回 `PUBLISH_TITLE_REQUIRED`。草稿仍须携带有效的 `target_store_id` 作为店铺上下文,并遵守已填写字段的格式和长度约束。SKU 发布标题 `variants[].title` 为 `null` 时继承 `profile.title`,不回退到 `display_name`;现有 SKU `tags=[]`、`description=null` 保持继承语义。标题、标签、描述分别独立覆盖。中俄对照按 SPU/SKU 与字段随 `editor-draft` 原子保存;SKU 继承项直接引用 SPU 对照,手工俄文覆盖但无匹配中文时标为对照待更新。发布适配器使用 SKU 有效文案仍是后续工作。
成功返回新 `product.revision`、`profile.revision`、创建的 SKU ID/`client_key` 映射和 warnings。任一字段、属性或媒体无效时返回 `422` 且不部分保存;版本冲突返回 `409 VERSION_CONFLICT`,客户端应重新读取编辑快照。
@@ -254,7 +261,7 @@ Body:`{ "variant_ids": ["uuid", "uuid"] }`,最多 100 个且不能重复。
没有报价时仍可保存商品,但首次发布预检返回 `PRICING_REQUIRED`。已确认报价不会随预览到期而失效;采购、包装、目标门店/类目、履约或门店币种改变后须重新确认。调整默认方案、汇率或费率不会反向改写历史快照。详见[定价 API](./pricing.md)。
开发环境可按定价 API 的草稿费率模式完成商品编辑联调;这种报价即使已保存,也会在首次发布预检返回 `PRICING_RATE_REVIEW_REQUIRED`。费率完成正式审核后,须用已审核卡重新试算并确认 SKU。
经维护者 2026-09-29 确认,使用草稿物流费率卡完成的报价可以保留到首次发布预检;未审核状态只作为来源信息展示,不再生成 `PRICING_RATE_REVIEW_REQUIRED`。当前仍要求 SKU 已确认有效报价、售价、包装和其他发布必填数据。生产环境默认可使用草稿卡,管理员可通过 `PRICING_ALLOW_DRAFT_RATES=false` 限制其进入新试算;审核流程仍可用于核实费率。
## 5. 素材选择与水印派生