商品按语言定价与 API 契约

商品编辑器及商品弹窗的每个语言标签(包括默认语言)均支持两种模式:

  • 自动换算:使用基础 products.price 与全局 locale_currency_bindings,基础划线价来自 metaData.original_price(兼容 compare_at_price)。
  • 自定义价格:独立选择币种、填写售价和可选划线价。划线价留空不显示,填写时必须高于售价。切回自动模式会移除当前语言覆盖。

配置保存在现有元数据中,无需新增数据库字段或迁移:

{
  "price": 10,
  "metaData": {
    "original_price": 20,
    "locale_prices": {
      "zh": { "currency": "CNY", "price": 99, "original_price": 199 },
      "en": { "currency": "USD", "price": 19 }
    }
  }
}

POST /api/admin/products 与 PUT /api/admin/products/:id 接受 metaData.locale_prices。语言键归一化为小写连字符形式(如 zh-HK 变为 zh-hk);归一化后重复的键被拒绝。商品覆盖只精确匹配当前语言,未配置的 zh-HK 不继承 zh 商品价格,仍按全局币种规则自动换算。删除键或传 {} 恢复自动模式;null、缺失或空售价、负数、非有限金额、不支持的币种、币种精度错误及无效划线价均返回 HTTP 400。零售价表示显式免费。

可选币种来自基础币种及已配置汇率的币种。自定义售价仍需真实汇率支持记账:基础币种汇率为 1;其他币种优先使用当前语言匹配的汇率,否则使用该币种唯一一致的全局汇率。缺失或有歧义时拒绝保存或下单,不猜测 1:1 汇率。

GET /api/products 与 GET /api/products/:slug 保留基础 price 并返回配置元数据。商品列表、详情、套餐卡片及 Product SEO Offer 使用共享规则按当前语言解析价格,缓存不绑定语言。

POST /api/orders/checkout 的 locale 决定售价,客户端金额不能覆盖服务端报价。阶梯优惠与推广折扣叠加作用于当前语言售价,再按成交币种精度取整;自定义实付除以锁定汇率得到基础币种记账金额。currencySnapshot 保存语言、实付币种及金额、汇率、基础金额、unitPrice、可选 originalPrice 和来源(自定义为 product-locale-price)。语言或商品配置变化不重算已有订单;只有快照一致才复用待支付订单。订阅续费继续沿用已购买订单的收款快照。

充值权益与售价独立:配置的 recharge_amount 按数量计算;未配置时,自定义语言售价仍沿用原基础售价、数量与折扣计算充值额度,服务端将到账额度单独锁入订单。

回滚边界:清除覆盖即可恢复自动换算;代码回退前须停止接收新配置并移除商品覆盖。已有订单保留金额及快照,不按新设置重算。支付方式币种限制仍由收银台和发起接口执行,无匹配支付方式时不能发起支付。