Herald 计费架构
Herald 的计费系统不维护本地商品目录。Stripe、Creem 这些支付方管理 Product、Price、Checkout 和订阅生命周期;Herald 只保留做授权和积分发放所需的最小数据——刚好够支撑一面与每笔支付、退款自动同步的付费墙。
Herald 的计费系统不维护本地商品目录。Stripe、Creem 这些支付方管理 Product、Price、Checkout 和订阅生命周期;Herald 只保留做授权和积分发放所需的最小数据。
付费墙闭环
这个模块里的一切设计都服务于同一个目标:把支付变成一面自动保持同步的付费墙。
- 用户通过 Stripe、Creem、WeChat Pay 或应用内购完成付款;
- Herald 把这笔购买解析成
entitlement_key,并向绑定的积分账户发放积分; - 你的产品据此检查权益或积分余额,放行或拦截这次请求;
- 退款和取消订阅会自动扣回积分;订阅权益任意退款即撤销,一次性购买的角色只在退款覆盖整笔支付时撤销,部分退款保留。
免费额度和滚动配额窗口走同一套模型,所以免费增值、订阅制、按量计费最终都收敛为同一次检查。
给谁看
需要理解 Herald 计费模块设计的开发者。不涉及具体支付方的对接操作,对接指南见各支付方的单独文档。
核心概念
积分账户
积分账户是积分的隔离单元。一个 Realm 可以建多个积分账户,各个账户的余额互相独立、互不影响。它解决的问题是:一个 Realm 下可能同时跑多条业务线,比如一个 AI 对话应用和一个图片生成应用,它们的积分要分开结算、分开消费,不能串。每个积分账户覆盖一组 Client App,只有被覆盖的 Client App 才能消费这个池的积分。
支付入账进 Entitlement Mapping 绑定的账户;注册与免费周期积分由 Realm 的发放规则路由。余额由逐笔发放的积分账本派生,沿五种积分类型(充值、会员、注册、免费周期、主动发放)分别展开。完整模型——字段、约束、账本与管理 API——见积分账户;消费侧见积分消耗。
Entitlement Mapping
Entitlement Mapping 是一张映射表,把支付方的外部商品 ID 映射到 Herald 内部的 entitlement_key,同时归属到一个积分账户。它不是商品目录,是 allowlist 加同步缓存。
每条映射包含:
| 字段 | 说明 |
|---|---|
payment_provider | 支付方名称(stripe、creem、apple、google、wechat) |
external_product_id | 支付方侧的商品 ID(如 prod_xxxx;IAP 为商店商品 ID) |
external_price_id | 支付方侧的价格 ID(Stripe 有,Creem 和 IAP 不适用) |
entitlement_key | Herald 内部的权益标识(如 pro-plan) |
bucket_id | 归属的积分账户。购买该商品后积分进入这个积分账户的池 |
billing_type | 计费类型:Recurring、OneTime 或 Non-renewing |
point_rules | 映射拥有的积分分发规则。留空是合法的——纯权益映射或纯支付记录(有效期、发放策略都配在规则上) |
granted_role_ids | 支付成功后自动授予的角色。空 = 不授予角色;配置步骤见支付墙配置指南 |
enabled | 是否启用。禁用后 webhook 仍更新订阅投影,但不触发积分发放 |
映射里还带一份从支付方同步过来的展示信息(provider_product_info),只读,用来在管理后台展示商品原貌,不参与计费:
| 字段 | 说明 |
|---|---|
| 产品名 | 列表和分组的主标签优先用它。缺失时回退到外部产品 id,再不行给占位,不显示空标签 |
| 价格 | 展示时按支付方区分单位。Stripe 同步的是最小货币单位整数(例如 999 表示 9.99),换算成主货币单位展示;Creem 直接用原值(字符串如 "9.99"),不再统一除以 100。WeChat Pay 没有可同步的目录——价格直接配置在映射上,以分为单位的整数,下单时直接作为订单金额。单位换算由支付方来源驱动,不跨支付方共用同一条换算分支 |
| 币种 | 商品币种,跟随同步覆盖 |
| 计费类型 | recurring / one_time / non_renewing;Stripe 和 Creem 从支付方同步,无目录的支付方(IAP、WeChat Pay)由管理员设置 |
| 计费周期 | Stripe 取 Price.recurring.interval(day/week/month/year),只读,不接受人工输入,重新同步以 Stripe 当前值为准。Creem 取其产品响应里的 billing_period(形如 every-month,前端做文案映射),缺失时显示"—",不伪造 |
| metadata | 同步 Stripe Product.metadata 和 Price.metadata(商户自定义键值对),只读展示。Creem Product 无原生 metadata,按空处理,不伪造 |
展示信息每次重新同步以支付方当前值为准覆盖本地;entitlement_key、points、grant 策略、quota_windows 等业务字段不被同步覆盖。
计费周期(来自 Stripe)和额度滚动窗口(quota_windows,如 5 小时 / 周)是两个独立概念,刻意解耦。计费周期决定 Stripe 何时扣款、何时发续费 webhook;额度窗口决定用户在滚动窗口内可用多少额度,两者不要求相等、不要求整除、不做对齐。长计费周期(年付)里仍可配滚动限额(月窗 / 周窗)。额度授予由续费 webhook 事件驱动,按 provider 返回的 (period_start, period_end) 锚定一期权益,不预发整周期总额、不按日历固定日清零。
映射通过三种方式产生:
- 管理员手动同步:调用支付方 Product API,拉取所有商品,自动创建或更新映射。新建的映射需绑定到本 Realm 的一个积分账户(必填,管理员可在同步后改挂到别的账户)。适用于 Stripe 和 Creem。
- 管理员手工创建:IAP 支付方(App Store / Google Play)和 WeChat Pay 没有商品目录同步,管理员直接建映射——IAP 用商店商品 ID,WeChat Pay 自定商品 ID 并在映射上配价格。
- Webhook 增量更新:支付事件触发时从 webhook payload 提取信息更新缓存。
同步失败时本地缓存继续服务,不会静默降级。
Subscription Projection
Subscription 是支付方订阅状态的本地只读副本,不是 Herald 拥有的订阅。SDK 和授权查询读取这个投影,不依赖实时支付方 API。
投影字段包括 realm_id、user_id、entitlement_key、bucket_id、status、current_period_start/end、provider_metadata 等。订阅在建立时就绑定到购买时那个 Entitlement Mapping 所属的积分账户,之后续费、升降级、取消、退款的积分发放和回收都回到同一个池,不会串到别的积分账户。需要计费周期或档位信息时,从 entitlement_key 或 provider_metadata 派生。
订阅状态:Active、Trialing、PastDue、Canceled、Expired、Paused、Disputed、ScheduledCancel、Incomplete。
has_access() 方法判断用户是否有权限:Active 和 Trialing 状态返回 true,其他返回 false。
支付尝试(Payment Attempt)
支付尝试是记录每一次成功支付的统一账本,覆盖一次性购买、订阅首期和订阅续费。订阅续费以前只更新订阅周期和积分,不留下"这次支付"的记录;现在每次成功续费也写一条已成功的支付尝试,并作为外部发票的本地归属锚点。每张第三方同步的发票都能直接定位到它归属的支付尝试或订阅。零金额周期(100% 折扣 / 免费档)不产生续费支付尝试。归属和异常发现的细节见发票管理。
Metadata 契约
所有 Herald 使用的 metadata key 统一用 herald_ 前缀。这些 metadata 写入 Checkout Session 或 Subscription,支付方在 webhook 中原样返回。
必填 metadata:
| Key | 说明 |
|---|---|
herald_realm_id | Realm ID |
herald_client_app_id | Client App ID |
herald_user_id | 用户 ID |
herald_entitlement_key | 权益标识 |
herald_billing_kind | subscription 或 points_package |
Checkout 创建时验证这些 metadata,缺失则拒绝创建。Webhook 收到事件后按以下顺序解析 entitlement_key:
- Webhook metadata 中的
herald_entitlement_key - 本地映射表(按 provider + external_product_id 查询)
- 都找不到则记录错误,不静默跳过
积分策略
积分策略的 source of truth 是 Herald 本地的 Entitlement Mapping,不是支付方的 metadata。Stripe 的 Product.metadata 和 Price.metadata 会跟随产品同步进入展示信息(仅 Stripe 适用),Herald 端只读,不回写、不编辑。Creem 无原生 metadata 功能,必须在 Herald 中手动配置。
策略按 entitlement_key 查询,覆盖:首次订阅发放、续费发放、取消回收、退款回收、升降级处理。发放和回收都定位到订阅绑定的积分账户池,按 credit_type 分别入账(积分类型的完整说明见积分账户)。
管理员可以在 Herald 中修改积分策略。修改的是 Herald 的业务规则,不会回写到支付方。
数据流向
用户支付流程:
- 前端调用 Checkout API,传入
entitlement_key和payment_provider - Herald 查 Entitlement Mapping,找到对应的外部商品和它归属的积分账户
- Herald 调用支付方 API 创建 Checkout Session,metadata 写入
herald_*字段 - 用户在支付方页面完成付款
- 支付方发 Webhook 给 Herald
- Herald 从 metadata 解析 entitlement_key,创建/更新 Subscription Projection,绑定到映射的积分账户
- Herald 按 entitlement_key 查积分策略,把积分发到该积分账户的池,或从该池回收
SDK 消费怎么路由
第三方应用通过 SDK 消费积分时只传 Client App 和金额,不感知积分账户。Herald 找到所有覆盖这个 Client App 的积分账户池,先扣配额窗口、再按过期时间从近到远扣池余额,原子完成、不超额。完整的消费语义——请求、扣减顺序、流水与回冲——见积分消耗。
支持的支付方
| 支付方 | 商业目录 | 订阅 | 一次性购买(积分包) | Metadata 支持 |
|---|---|---|---|---|
| Stripe | Product / Price API | Checkout Session + Subscription | Payment Intent | Product/Price/Checkout/Subscription 均支持 |
| Creem | Product API | Checkout | Checkout | Checkout metadata,webhook 返回 |
| Apple App Store IAP | App Store Connect(无同步,手工建映射) | StoreKit 2 + 服务端通知 V2 | 消耗型,凭 receipt 提交履约 | 经 appAccountToken 绑定用户 |
| Google Play Billing | Play Console(无同步,手工建映射) | Play Billing + 定时轮询(无 RTDN) | 消耗型,凭 receipt 提交履约 | 经 obfuscatedExternalAccountId 绑定用户 |
| WeChat Pay | 无(按订单定价,手工建映射) | 仅 non-renewing——固定期、单次付款、不自动续费 | 统一下单(PC 扫码 Native / 微信内 JSAPI) | 无目录 metadata;价格与币种配置在映射上 |
Stripe、Creem、WeChat Pay 是发起式平台,通过 PaymentAttempt 统一管理支付过程。WeChat 订阅到固定截止日为止——没有续费扣款,用户重新购买续期。IAP 同样经 PaymentAttempt 入账,但履约由移动 App 提交购买凭证(Apple jwsRepresentation / Google purchaseToken)触发,而不是 checkout webhook——平台通知(Apple)和定时轮询(Google)只负责之后的生命周期。详见 IAP 对接指南 与 WeChat Pay 对接指南。
多货币
货币挂在目录本身:每个 Stripe Price 自带货币,一个产品卖多种货币就是多行映射。没有默认货币——购买页展示全部已配置货币,由用户显式选择;按货币解析是严格的——不回退第二个货币,不产生零金额扣款。Creem、IAP、WeChat Pay 保持单一价格(WeChat 的手工价格与币种配置在映射上)。详见多货币定价指南。
相关文档
- 支付墙配置指南 — 在映射上配置授予角色:支付授予、退款回收、应用侧检查
- 积分账户 — 积分账户模型:字段、覆盖集、积分类型与账本
- 积分消耗 — 消费调用、扣减顺序、流水记录与回冲
- 积分账户用户故事 — 积分账户目录、覆盖集、购买入池、跨池消费、订阅生命周期的验收场景
- Stripe 对接指南 — Stripe 支付方配置和 Webhook 处理
- 多货币定价指南 — 货币偏好、购买页分组与按货币解析价格
- Creem 对接指南 — Creem 支付方配置和 Webhook 处理
- IAP 对接指南 — App Store / Google Play 内购配置、receipt 提交与生命周期处理
- WeChat Pay 对接指南 — WeChat Pay 配置、按订单定价的映射与 Native / JSAPI 购买流程
- 发票管理 — 发票创建、开票、PDF 生成