feat(pricing): 开发定价方案

This commit is contained in:
R524809
2026-09-28 17:34:33 +08:00
parent a29dd92676
commit 84b230e69a
54 changed files with 6264 additions and 236 deletions
@@ -0,0 +1,103 @@
# SKU 定价试算与确认:产品及实施提案
> 2026-09-28 · 维护者已批准本方案,进入实施。第 5 节的契约变更同步到 API 文档与共享契约;原型示例数据不作为生产费率。
## 1. 结论
建议采用「SKU 就地自动试算 + 详细试算弹窗 + 商品页统一保存」。采购价失焦或按 Enter 后,用该 SKU 当前方案计算并回填销售价、划线价和预估净利润;保留“计算”按钮供主动重算。用户可以关闭自动试算。弹窗用来查看费用、比较物流、调整本次参数,点击“应用到 SKU”回到未保存表单。商品页“保存”才确认报价并持久化。
把用户的两种意图分清:输入成本时需要立即看到结果;保存商品时才承诺这次售价。避免每个 SKU 都弹出一次确认框,也避免一次失焦就写入正式报价。
## 2. 已阅读的依据与当前差距
- 新项目:`requirements/pricing-tool.md`、`requirements/cel-ozon-rfbs-rates-review.md`、`api/pricing.md`、`api/product-editor-and-publishing.md`、`architecture/exchange-rates.md`,以及现有 pricing 表和 SKU 表单。
- 旧项目:`studio/src/pages/trial/TrialPage.tsx` 的「2 价格试算」、`TrialPricingPanel.tsx`、`studio/src/pricing/pricing.ts`。
- 旧页面通过 `recalc()` 在参数变化时计算,并通过 `onSave()` 保存参数、售价、划线价与汇率。左右布局、包装数据带入、费用明细和中俄币种对照值得保留。
- 旧公式把采购成本上的加价率叫“净利率”,使用固定 12%/18% 佣金和 3.5% 其他扣费;low/high/high2 不能表达新的 CEL 货值类型与运输方式,不适合直接迁入 Atlas。
- Atlas 目前的 SKU 摘要已有采购价输入、只读售价和“净利润 —”;定价工具还是占位页。已有 pricing 基础表,但预览服务、报价确认、汇率 Worker 和方案管理尚需实现。
- 现有 `pricing-confirms` 契约独立落库,而本提案建议随草稿保存确认;不能只改前端按钮文案,必须先调整保存事务与契约。
## 3. SKU 与弹窗交互
### 3.1 SKU 卡片
摘要行保留:采购价 CNY、销售价(目标店铺币种)、预估净利润 CNY、“计算”、“试算详情”。第二行以较轻的文字显示方案、采用的运输方式、报价状态;包装缺失时显示具体缺项和补充入口。
自动算价默认开启,作为用户的编辑偏好。有效值发生变化且字段失焦/按 Enter 后触发,不在每次键入时创建请求。采购价、有效包装规格、目标店铺、类目或 SKU 参数变化均使之前的结果失效;同值失焦不重复请求。包装继承关系仍由 SPU/SKU 原有模型解析。
SKU 状态为:未试算 → 计算中 → 已试算·待保存 → 已确认。缺少资料、请求失败或参数已变时显示可修复状态,不把旧利润继续当成当前结果。
“按方案计算全部”逐 SKU 返回结果;成功项更新表单,失败项保留输入并标出原因。手动售价不被批量推荐价覆盖,手动项按原售价重新核算利润。
### 3.2 手动售价
用户可直接修改售价,系统切换到“手动售价”模式,按这个价格计算物流适用性、佣金及利润。此后修改采购价或包装数据,保留售价,仅更新成本和利润。“使用建议售价”是显式退出手动模式的动作。
预估净利润只读,不接受手填。标签使用“预估净利润”;它是扣除已配置费用后的预计利润,不是账单结算利润。低于目标或为负时明确显示差额。首期建议亏损报价可保留为草稿,但不得自动成为可发布报价;后续若允许强制确认亏损价,需另行确定权限与确认规则。
### 3.3 试算弹窗
- 顶部:SKU 名称、当前目标店铺及币种、定价方案。进入时复制该 SKU 的表单输入与参数,取消不修改 SKU。
- 左侧:采购价、有效包装重量/尺寸、目标加价率、划线折扣率;高级费用折叠展示国内运费/贴单、广告、尾程、提现、其他费、汇率来源和临时覆盖。
- 页面中的加价率、折扣率、广告、尾程、提现、其他费用率及临时佣金率均按百分数展示和输入,统一保留两位小数;API 仍使用十进制比例字符串,例如页面 `50.00%` 对应请求 `"0.5"`。
- 右侧:大号销售价、划线价、预估净利润及利润占销售额比例;费用明细与规则来源按需展开。
- 物流比较:展示方案默认运输方式,同时允许比较其他可用方式。最低价只是标签,不自动替换方案指定的运输方式;指定方式不可用时要求用户选择。
- 修改参数后延迟约 400 ms 试算,输入未完整时不发送请求;取消旧请求并校验返回版本,防止旧结果覆盖新参数。
- 底部:取消 / 应用到 SKU。应用只更新当前 SKU 的待保存报价和本次参数,不修改门店默认方案。
- 用户在弹窗修正包装时,明确写入该 SKU 的包装覆盖值,不修改 SPU 默认规格;原型只演示数值,正式组件需显示继承来源并提供恢复继承操作。
### 3.4 独立定价工具与定价方案
独立“即时试算”和 SKU 弹窗使用同一输入组件、结果组件与服务端引擎。独立试算不要求已有商品,不提供“应用到 SKU”。
“定价方案”负责门店范围的默认扣费、利润目标、折扣、汇率策略和运输偏好;每个门店最多一个默认方案。新 SKU 首次计算时采用它,已有 SKU 继续使用自己的方案/本次覆盖。方案编辑不重算历史已确认报价。将临时参数另存方案应是独立明确动作,采购价和包装尺寸不随之保存。
## 4. 保存、变化与失效
| 操作或事件 | 表单结果 | 已确认快照 |
|---|---|---|
| 成本失焦,输入完整 | 自动预览并回填,标记待保存 | 保留 |
| 弹窗点击应用 | 覆盖当前 SKU 的待保存报价 | 保留 |
| 手改售价 | 保留手动价,重算费用/利润 | 保留 |
| 包装/类目/店铺变化 | 旧预览失效;重算或提示缺项 | 保留历史,新草稿标记未满足发布报价条件 |
| 方案或系统汇率更新 | 不静默替换已确认价格 | 保持原输入、版本、汇率与结果 |
| 点击商品页保存 | 服务端核对并确认本次报价 | 生成新不可变快照,并切换 listing 引用 |
| 预览过期或规则在保存前变化 | 重算并展示新旧价格/费用差异 | 未经重新接受不替换 |
| 取消弹窗/离开未保存页面 | 放弃相应临时修改 | 不变 |
保存不能因为某个尚未定价的 SKU 阻止保存整个商品草稿;该 SKU 可保存为“待定价”,发布时阻断。对于携带了报价确认意图的 SKU,只要存在过期/版本冲突,就返回具体 SKU 错误,整个保存事务回滚,不悄悄丢弃确认意图。界面可提供“仅保存草稿,稍后定价”的明确后续操作。
已确认报价不因时间经过而自动过期;15 分钟有效期只作用于未确认预览。影响报价的商品输入改变后,当前草稿必须重新确认。发布时检查快照与待发布输入一致,已导入 SKU 沿用既有编辑边界。
## 5. 后端及契约建议
维护者已批准以下变化:
1. **预览上下文**:扩展现有 preview 契约以识别 `profile_id + variant_id`,并接收明确的未保存采购价、包装覆盖和本次参数。服务端校验资源归属,并按与草稿保存相同的逻辑合并包装。未保存的新 SKU 需支持当前商品内的客户端草稿标识,保存时原子映射到真实 SKU ID;或首版在首次保存后才开放报价确认。
2. **两种定价意图**:支持目标加价率求售价,以及固定售价反算利润。输出统一的售价、划线价、利润、费用和规则快照。金额使用十进制定点计算,HTTP 仍传字符串。
3. **预览标识**:预览绑定用户/工作空间、平台/店铺、SKU、规范化输入摘要、规则/方案/汇率版本及有效期。可保存在有过期清理的 PostgreSQL 预览表;不得只凭客户端提供的金额确认,不以 Redis 作为确认事实来源。
4. **统一保存**:在 `editor-draft` 中增加每个 SKU 的可选 `pricing_selection`(preview id、候选 id、输入摘要),并为包含报价确认的保存提供幂等键。草稿、SKU、报价/候选快照、listing 售价与报价引用在同一事务提交,沿用 product/profile revision 乐观锁。
5. **复用确认逻辑**:将当前规划的 `pricing-confirms` 行为放入可被草稿保存事务调用的领域服务。是否保留独立 HTTP 入口需要统一决策;不能采用前端“先保存草稿,再逐个确认报价”来伪装原子保存。
6. **严格的同归属约束**:补齐报价与 product/profile/variant/listing 的复合外键和工作空间 RLS;现有基础 DDL 中 `pricing_quotes.product_variant_id` 仅是逻辑关联,不能直接当作完整领域约束。
7. **确认校验**:检查预览输入和待保存草稿一致、版本可用、候选有效、币种及舍入后结果一致;不在确认请求中重新抓取外部汇率。未变化的已确认 SKU 不重复创建报价。
8. **币种与单位**:售价和划线价以目标店铺支持的币种持久化,CNY 成本为统一输入,RUB 阶梯按本次汇率判定;明确现有 RUB-only 报价列与 CNY 店铺售价的映射。界面可切换展示单位,内部统一 g/mm 或规范化 kg/cm,禁止混算。
## 6. 计算引擎
复用旧项目的拆分方式与展示经验,重写纯函数计算器。服务端引擎是业务事实来源,前端只负责预览展示。所有入口调用同一引擎。
目标加价率基于采购成本:目标利润 = 采购成本 × 加价率。固定成本包含采购、最终跨境物流、国内运费/代贴单及固定附加费。各百分比费用基于最终销售价;划线价 = 销售价 ÷ (1 − 划线折扣率)。
引擎枚举“运输方式 × CEL 产品类型 × 佣金档”,计算售价后回验货值范围、重量、尺寸、计抛、佣金边界和最终舍入价格。固定售价模式直接以输入售价确定区间并计算利润。无自洽候选时返回原因,不任意选档或无限迭代。
CEL 表尚需真实账单复核;佣金映射与规则也需审核版本。开发可先使用明确标记的测试规则与方案固定汇率推进全链路;正式环境缺少有效规则时禁止伪造价格。CBR 更新、规则发布和过期提示按现有文档落地。
## 7. 开发顺序与验收
1. **确认交互及契约**:先确认自动失焦回填、手动价保护、随草稿保存这三个选择;再更新需求/API/contracts。影响 Web、API、Worker 汇率任务、database 和 contracts,不复用旧 SQLite 或无鉴权 API。
2. **引擎和规则**:实现 Decimal 计算、CEL/佣金版本读取、单位转换、候选枚举和固定价反算;测试价格跨档、重量/体积重边界、取整后跨档、总费率 ≥100%、负利润及不可报价。
3. **首个垂直交付**:一个已保存 SKU、一个门店方案、采购价失焦预览、弹窗改参数、统一保存、刷新重读及发布预检引用报价。此阶段就验证租户隔离、旧响应丢弃、幂等重放、并发版本冲突、确认失败整笔回滚、方案变更不改历史报价。
4. **扩展编辑效率**:多 SKU 批量试算、缺项定位、包装继承、新 SKU 保存映射、手动售价保护、预览过期后的差异接受。
5. **完整工具页**:复用同一组件和引擎补独立试算、方案 CRUD、默认方案、汇率同步及真实账单复核。试算记录列表后置。
首轮验收:修改采购价后能在 SKU 行看到新售价与利润;弹窗取消不改草稿;应用只改对应 SKU;成本变化不覆盖手动价;保存后重读能解释该价格所用的包装、汇率、佣金和物流规则;失败 SKU 不阻塞其他 SKU 的试算;缺少报价的草稿可保存但不可发布。