多货币定价
让一个 Stripe 产品以多种货币销售。覆盖购买页货币分组与显式选择,以及面向第三方应用的 api-ext 按货币解析接口。
让一个 Stripe 产品以多种货币销售。覆盖购买页货币分组与显式选择,以及面向第三方应用的 api-ext 按货币解析接口。
适合谁看
- 想让同一产品以多种货币销售的 Realm Admin
- 把 Herald 购买流程接进自己应用、需要知道权益支持哪些货币以及如何按货币解析价格行的开发者
前置条件
- Stripe 已配置,且你能执行产品同步(见 Stripe 对接指南)
- 知道 Entitlement Mapping 是什么(不知道的话先看计费架构)
核心概念
Herald 没有本地商品目录。价格挂在 Stripe 的 Price 对象上,同步时每个 Price 生成一条独立的 Entitlement Mapping 行。一个配了 USD 月付、USD 年付、EUR 月付的产品,同步后就是三行映射。多货币能力完全建立在这个模型上:不新增任何存储,每行自带的货币就是全部事实。
没有默认货币,也没有用户级偏好货币。购买页展示全部已配置货币,用户显式选择一个之后才出现价格行;应用侧则由调用方显式传 currency 参数解析。无论哪一侧选择,最终扣的都是被选中的那行映射。
货币本身定位不了一行价格。同一产品可以同时有 USD 月付和 USD 年付,所以货币只是过滤条件,不是唯一键——(权益 + 计费类型 + 计费周期 + 货币) 才能锁定一行。凡是解析结果不是恰好一行,Herald 都会拒绝而不是猜一个。不会出现静默换币,也不会出现零金额扣款。
第 1 步:在 Stripe 搭建多货币目录
- 在 Stripe Dashboard → Products 打开你的产品
- 每种货币加一个 Price,比如在
$9.99 / month之外加€9.99 / month和¥69 / month - 在 Herald 打开 Entitlement Mappings,执行 Sync Provider Products
每个 Price 到达后各占一行映射,带各自的货币和金额。把要卖的行启用。禁用的行不会出现在购买页,也不会进入提供给第三方应用的货币集合。
第 2 步:用户在购买页看到什么
Stripe 产品有多个货币的价格时,购买页按货币分组,每个货币是一个可选按钮。没有预选货币:用户显式选择之前不渲染任何价格行——页面显示「请选择货币」的提示。选中的组内,每个计费周期一行(月付、年付并排放在 USD 组里)。
只有一种货币的产品不显示选择器——唯一的货币直接展示价格行,因为没什么可选。
其他渠道的产品也没有选择器。Creem 按产品定价,IAP 按商店地区定价,WeChat Pay 的价格手工配置在映射上——它们都只展示单一价格。
一个 Stripe 侧的注意点:如果你在 Stripe 启用了 Adaptive Pricing,Stripe 收银台可能把金额换算成买家本地货币。Herald 购买页始终展示 Price 上的基础货币,并标注「实际扣款以支付页为准」。Herald 自己不做任何换算。
第 3 步:在应用里按货币解析价格
第三方应用用两个接口处理货币。都走 API key(X-API-Key 请求头)加 billing.view 权限,且都只统计已启用的 Stripe 映射行。
查询权益支持的货币集合:
curl -H "X-API-Key: your-api-key" \
"https://your-herald-domain/api/ext/{realmId}/entitlements/{entitlementKey}/currencies"{
"entitlementKey": "pro-plan",
"currencies": ["USD", "EUR", "CNY"]
}按货币解析默认价格行,可用计费类型和周期收窄范围:
curl -H "X-API-Key: your-api-key" \
"https://your-herald-domain/api/ext/{realmId}/entitlements/{entitlementKey}/default-price?currency=EUR&billingType=recurring&billingPeriod=monthly"{
"mappingId": "0198c7e4-...",
"entitlementKey": "pro-plan",
"paymentProvider": "stripe",
"currency": "EUR",
"amount": 999,
"billingType": "recurring",
"billingPeriod": "monthly",
"externalPriceId": "price_..."
}拿响应里的 mappingId 作为购买支付尝试的 targetId 发起购买——和浏览器购买页走的显式选行是同一条路径。
失败行为是有意区分的:
400—— 货币码缺失或格式非法404—— 该权益在这个货币下没有已启用的 Stripe 行。不会回退到任何其他货币,怎么提示用户由调用方决定409—— 匹配到多行(比如只传currency=USD时同时存在 USD 月付和年付)。补上billingType或billingPeriod再试
货币码与目录比较时不区分大小写,响应统一返回大写 ISO 码(usd 和 USD 是同一货币)。订阅类和一次性映射都带货币——订阅详情和一次性映射响应里有 currency 字段。
容易踩的规则
货币永远是显式选定的。 没有 Realm 默认货币、没有用户偏好、没有回退链。购买页在用户选定货币后才渲染价格行;程序化接口只按你传入的货币解析一次,解析不了就报错。这正是为了不让用户被以一个从没选过的货币扣款。
Stripe 行缺价格信息会被拒绝,不会补默认值。 Stripe 映射行万一缺了价格数据,购买会以明确错误被拒绝,而不是编一个金额发出去。健康的行上显式 targetId 购买不受影响。store 侧定价渠道(Creem、IAP、WeChat Pay)恰好相反:它们的行本来就不带 Herald 侧价格,因为订单金额由渠道决定。WeChat 映射是唯一的手工场景——价格和货币在创建弹窗里填写,随每笔订单下发。
排查
刚加的货币 default-price 返回 404
依次确认:映射行是 Enabled;行的 provider 是 Stripe(Creem/IAP/WeChat 行永远不会被考虑);在 Stripe 加完 Price 之后确实重新同步过。
default-price 返回 409
该权益在这个货币下有多行——常见是月付加年付。补上 billingType 或 billingPeriod 再试。
default-price 拒绝我传的货币码
货币码必须是恰好 3 个大写字母,XXX/XTS 是保留码。美元、带空格的 usd、4 位码都会以 400 被拒,不会进入解析。
检查清单
- Stripe 产品为每个要卖的货币各配了一个 Price
- 执行过 Sync Provider Products;每个货币行可见且 Enabled
- 用非默认货币完成过一次购买;扣款货币与所选行一致
- (应用)渲染货币选择器前先调
currencies;404/409有面向用户的处理