FlareStarter 文档
功能与集成

计费与订阅

基于 Stripe 的订阅(月付/年付)一次性终身买断,带幂等 webhook、套餐门控(requirePlan)、计费事件钩子。本页讲权益模型webhook 处理、以及**扣款失败(dunning)**怎么分工。

权益模型(entitlement.ts)

getEntitlement() 返回一个 Entitlement:{ plan, status, isActive, currentPeriodEnd, lifetime, paymentFailed }

  • plan:free | prorequirePlan('pro') 据此做路由门控;role === 'admin' 直接放行(角色高于付费墙)。功能门控用 hasProAccess(role, ent),计费 UI(套餐徽章/升级按钮)保持用 ent.plan 反映真实订阅。
  • lifetime:终身买断优先于一切 status(即使 canceled 仍是 Pro)。
  • paymentFailed:续费扣款失败的站内信号(见下文)。

Webhook 处理(开源版范围)

translateStripeEvent 把 Stripe 事件翻成领域事件,落库时幂等(processed_webhook_events 去重):

Stripe 事件处理
customer.subscription.created/updatedupsert 订阅状态;active/trialing → Pro 门控开,past_due 等 → 关
customer.subscription.deleted降级 free
checkout.session.completed(mode=payment)终身买断激活(仅 payment_status=paid;延迟到账时不预授)
checkout.session.async_payment_succeeded延迟到账支付(ACH/SEPA 等)入账 → 终身买断激活
charge.refunded(全额)终身买断退款 → 降级
invoice.payment_failedpaymentFailed flag(站内 banner),触发 onPaymentFailed 钩子
其余(invoice.paid / dispute 等)忽略(商业版扩展点)

扣款失败(Dunning):Stripe 负责重试,app 负责站内提示

重试与催缴邮件交给 Stripe——它做得比手搓的好,而且零代码:

  1. Stripe Dashboard → Settings → Billing → Subscriptions and emails
  2. 开启 Smart Retries(智能重试失败扣款)。
  3. 开启 failed payment 的客户邮件(自带"更新支付方式"的托管链接)。
  4. 可选:配置重试耗尽后是 cancel 还是 mark unpaid 订阅。

模板只补 Stripe 给不了的那一件事——站内信号:

  • 收到 invoice.payment_failed → 在订阅行置 payment_failed_at(billing.server.ts)。
  • resolveEntitlement 据此暴露 entitlement.paymentFailed
  • 登录后的 app 顶部显示 PaymentFailedBanner:"上次扣款失败,请更新支付方式" → 跳 Stripe Customer Portal 换卡。
  • 扣款恢复(订阅回到 active/trialing)时,flag 自动清除

为什么不在 app 里发阶梯催缴邮件?那会和 Stripe 的能力重复、且节奏脆弱。分工后,dunning 不需要 Cron——这也是 Cron 在开源版被定性为"清理任务参考实现"而非计费基建的原因(见 cf-gotchas)。

微信支付(赞助页的一次性付款)

微信支付走 Stripe 的 wechat_pay,不需要独立的微信商户号——对本系统而言它只是"一笔非 USD 计价的普通 Stripe 收款",webhook / 退款 / 幂等全部复用现有链路。

只在赞助页(/sponsor)的一次性赞助上提供,原因是 Stripe 的 wechat_pay 不支持 subscription / setup mode(recurring 目前仍是 private preview)。所以:

  • 月度赞助、Pro 月付/年付订阅 → 没有微信入口(前端不渲染按钮,后端一并拒绝)。
  • Pro 终身买断虽然是一次性付款,但当前用预设 Price ID(USD)结账,未接微信——需要时按 sponsor.stripe.ts 的写法照搬即可。

收款币种与固定汇率

wechat_payStripe 商户国限制计价币种:cny 所有国家可用、hkd 仅 HK 账户、usd 仅 US 账户,其余各限对应国家。HK 账户不能用 USD 收微信款——所以配置里给了一个固定汇率:

// src/features/sponsor/sponsor.config.ts
wechat: {
  currency: 'hkd',  // 必须匹配你的 Stripe 商户国
  usdRate: 7.8,     // 1 USD 折多少 currency
}

定价基准仍是 USD(档位/自定义金额都是美分);usdRate 只在创建 Checkout Session 那一刻换算一次,结果随即冻结进订单:

含义
amount + currency实收额(微信为 HKD 分)。对账 Stripe 流水的口径,永不换算
amount_usd冻结的 USD 分等值(写入 session metadata 再由 webhook 回读)

赞助墙的累计分层只用 amount_usd——它把同一 GitHub 的多行金额相加再比阈值,混币种直接相加会把 HK$78 算成 $78 顶进金牌档。冻结而非读取时换算,意味着日后调 usdRate 只影响新订单,历史订单展示不漂移。换算逻辑与单测在 src/features/sponsor/currency.ts

顾客在微信收银台看到的始终是换算后的人民币金额,与 currency 填什么无关。

上线前提与开关

  1. 必须先在 Stripe Dashboard → Payment methods 为该账户开通 WeChat Pay。这一步无法从代码探测,未开通时用户点击会由 Stripe 报错。
  2. 入口默认开:STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET 配好即显示。
  3. kill switch:设 STRIPE_WECHAT_PAY_ENABLED=false 即隐藏按钮并拒绝下单。

退款说明

微信支付没有顾客发起的拒付/争议(Stripe 原文:"there is no dispute process that can result in a chargeback"),退款只能由商户在 Dashboard 手动发起,窗口 180 天、异步到账。全额退款照常触发 charge.refunded → 赞助行置 refunded 并下墙(与卡支付同一条路径)。

计费事件钩子(hooks.ts)

BillingHooks 让你在状态跃迁后挂副作用(best-effort,失败不影响 webhook 返回 200):

  • onProActivated(ctx, via) —— 例:发"Pro 已开通"邮件(默认已示范)。
  • onProDeactivated(ctx, reason) —— 例:回收已开通资源。
  • onPaymentFailed(ctx) —— 例:发 Slack 提醒。重试邮件别在这发(Stripe 已发)。

编辑 src/features/billing/hooks.ts 接你自己的逻辑。

本地开发

留空 STRIPE_* key 时,计费入口优雅降级(无结账按钮)。测试 webhook 用 Stripe CLI:

stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger invoice.payment_failed   # 验证站内 banner 出现

On this page