← 返回博客
Billing发布于 2026-03-21· 8 分钟阅读

Next.js 订阅计费:真正能用的 Webhook 契约

By make1day · Updated 2026-11-07

多数订阅计费教程止步于顺利路径:创建结账会话、跳转、结束。生产级计费恰恰活在那些被跳过的地方。

Webhook 才是真相来源

浏览器跳转会骗人:用户关掉标签页、支付商重试、测试触发两次。数据库必须是唯一真相来源,且只由已验签的 Webhook 更新。

验签契约值得写清楚,因为写反了就是安全漏洞:

  • 签名缺失、无效或格式损坏 → 抛错,路由返回 400。
  • 合法已验签但无需处理的事件 → 返回空列表,路由返回 200 并标记 ignored。
  • 已见过的事件 id → 写库前直接跳过。

本仓库每个适配器都遵守这份契约。早期版本在验签失败时也返回空列表,等于用 200 确认了伪造请求,别再退回去。

权限在服务端,不在界面

永远不要信任客户端。每个受保护路由都从会话或数据库读套餐;套餐未激活时 AI 接口返回 402——哪怕有人在 devtools 里改了 DOM。

预算和配额是两件事

订阅状态不等于用量限制。AI 接口另有每用户每日配额(AI_MESSAGES_PER_DAY,默认 50),由 Supabase RPC 或本地 SQLite 的 ai_usage 表扣减。用尽返回 429,带 X-AI-Quota-Reset 与按真实剩余秒数计算的 Retry-After,不是写死的 86400。

离线也能测

验证这份契约不需要六个沙箱账户。仓库提供离线验签探测(npm run probe:webhook),向运行中的服务发送合法、缺失、篡改、损坏四种请求并断言状态码。上线前仍要跑沙箱端到端,但安全不变量可以在 CI 里守住。四层测试策略见 docs/payment-testing.md。

FAQ

Webhook 怎么做幂等?+

把支付商事件 id 记进 webhook_events 表,在写套餐或 license 之前先查。验签在最前面,只有合法且未处理的事件才进入去重步骤。

402 和 429 有什么区别?+

402 表示没有下载凭据(无有效授权码、订阅或邮箱认领)。429 表示 AI 每日消息配额用尽。预览额度耗尽是另一个 402,错误码为 preview_limit_reached。

没有沙箱账户能测六个通道吗?+

共用验签契约可以用 npm run probe:webhook 离线测;每个通道的真实结账流程上线前仍需各自的沙箱凭据。所需环境变量见 docs/payment-testing.md。

All frequently asked questions · Markdown version (for AI agents)

Next.js Subscription Billing: The Webhook Contract That Actually Works · make1day