支付与 Webhook

追踪 Checkout、Stripe 回调、交易记录和权益发放,定位付款后未开通等问题。

支付排错应追踪同一笔业务的 Checkout ID、Stripe Event ID 和内部执行记录。不要为了复现问题反复真实付款,也不要只凭支付成功页判断账号已经获得权限。

一、先核对连接和套餐

配置作用常见错误
payment.provider当前支付提供商配置与实际调用不一致
STRIPE_CONNECTION_ID应用内部的 Stripe 连接标识已有数据后随意更换,导致找不到原记录
STRIPE_SECRET_KEY调用 Stripe API测试、正式模式或账号不一致
STRIPE_WEBHOOK_SECRET验证回调签名使用另一个 Endpoint 或 CLI 监听器的 Secret
产品及套餐 Key查找应用套餐前端提交的 Key 已改名或不存在
套餐 priceId对应 Stripe Price仍有占位值,或 Price 属于其他模式

STRIPE_CONNECTION_ID 是项目自己的稳定标识,不是 Stripe 自动提供的 Price ID。测试与生产应分别配置,已有生产记录后不要随意更换。

npm run doctor 和 npm run doctor:remote 能提示变量缺失,但不会验证每个 Price 的归属和可购买状态。套餐展示价格也不会改变 Stripe 的实际收费。

二、无法创建 Checkout

在 Network 中检查创建 Checkout 的请求,先区分参数校验、登录要求、重复购买限制和 Stripe API 错误。

如果 Price 仍包含 _replace_me,先在相应 Stripe 模式中创建价格,再填写真实 ID。一次性套餐与订阅套餐还要使用适合其支付方式的 Price。

出现 paymentsProductAlreadyPurchased 时,检查 allowRepurchase 和用户已有的有效购买记录。不要通过删除交易历史让用户再次付款。若业务需要允许重复购买,应调整套餐规则并测试购买后的权益处理。

重复购买判断也不是并发锁。多个标签页可能在第一笔付款完成前创建多个 Checkout Session,测试时只保留一个有效流程。

三、本地接收不到 Webhook

本地开发地址不能直接作为 Stripe 从公网投递的目标。可以使用 Stripe CLI 将测试事件转发到开发服务器:

stripe login
stripe listen --forward-to http://127.0.0.1:5173/api/webhooks/stripe

把端口替换成 npm run dev 实际监听的端口,将监听器输出的签名 Secret 填入本地 .env 的 STRIPE_WEBHOOK_SECRET,然后重启开发服务器。该值与控制台创建的 Endpoint Secret 不能混用,详见 Stripe 本地监听与回调说明。

随后从应用页面创建一笔测试 Checkout,再观察 CLI 投递、服务端日志和本地数据库。单独触发一个模拟付款事件,可能缺少应用创建的 Checkout、用户关联或套餐映射,不能据此断定整个购买流程失败。

项目也提供 webhook:stripe:dev 与 webhook:stripe:prod,它们会创建远程 Webhook 目标并把 Secret 写回相应环境文件,不是本地转发器。使用这些脚本时需要提供可公开访问的地址,已有目标时先核对,不要反复创建。

四、回调签名、路径或配置错误

当前入口是:

POST {VITE_SITE_URL}/api/webhooks/stripe
现象检查顺序
404、405完整地址、HTTP 方法、目标 Worker 是否为当前版本
签名失败Endpoint 或 CLI Secret、原始请求体、Stripe-Signature 请求头
PAYMENT_NOT_CONFIGURED线上 Worker 的连接 ID、API Key、Webhook Secret
5xx服务端最早的异常、Stripe API、D1 和业务回调处理
WEBHOOK_BUSY同一事件是否已有处理租约,查看时间和对应日志

签名校验使用原始请求体。不要先解析 JSON 再重新序列化,也不要为排错关闭验签,参见 Stripe 签名排错。

修改本地 .env.production 后还需要更新部署。文件已经写好,不表示 Worker 运行时 Secret 已经更新。域名变更时也要检查 Stripe 目标地址是否仍指向旧域名。

五、从事件账本确定处理位置

在正确的业务库 DB 中,只读查询目标 Stripe Event。把示例标识替换成本次事件 ID:

SELECT id, provider, connection_id, event_id, event_type,
       status, created_at, updated_at
FROM webhook_events
WHERE provider = 'stripe'
  AND event_id = 'evt_replace_me'
ORDER BY id DESC
LIMIT 10;

同一事件还需要结合 connection_id 判断归属。不要把完整 raw_body 导出到共享日志,它包含业务和用户信息。

结果含义与下一步
无记录检查是否投递到正确环境、是否通过验签,以及事件类型是否受支持
processing查看租约、更新时间和当前执行日志,不能只凭状态断定处理仍活跃
failed在受控环境查看 last_error,定位失败处理步骤
succeeded继续核对交易和权益,不代表所有后续异步任务完成

当前 webhook_events 没有 attempt_count 字段。投递次数查看 Stripe 的投递历史,Command 尝试次数查看 command_execution,两者不是同一个计数。

不受支持的事件会被记录为 payment.webhook.ignored 日志并返回成功,不会进入该账本。已处理成功的重复事件也会直接返回成功,因此 2xx 不能单独证明新增了一笔交易。

六、已经付款,但没有获得角色或权益

按以下顺序查找最后一个成功步骤:

Stripe 付款状态
→ 对应回调投递及验签
→ checkout_sessions / transactions / transaction_items
→ 套餐与 Price 映射
→ event_execution / command_execution
→ user_role_capability / user_entitlement_capability

一次性支付和订阅使用的业务事件不同,不能要求每笔订阅都必须找到 PaymentSucceededEvent。订阅还应检查 subscriptions、subscription_items 及订阅创建、更新、结束事件。

重点检查以下情况:

  • 回调里的 Price ID 没有映射到当前产品套餐,无法生成预期授予输入。
  • 定制后的套餐没有配置需要授予的角色或权益。
  • 支付记录已更新,但 Event 创建或 Command 执行失败。
  • 权益已经保存,但页面检查的是另一个能力 Key、用户或来源。
  • 用户完成了 Checkout 页面操作,但支付方式还处于异步确认阶段。

后台执行记录的查询和状态说明见后台任务与统计。确认根因前不要手工新增交易或直接给账号加权限,否则会掩盖故障并破坏后续撤销关系。

七、重复投递、忙碌状态与重放

Stripe 可能重复投递,事件顺序也不应被视为业务发生顺序,参见 Stripe Webhook 投递行为。

模板按提供商、连接 ID 和 Event ID 识别事件。成功记录直接返回成功,有效处理租约占用时返回非 2xx,允许提供商稍后重试。当前 Stripe 处理租约的最大年龄为三分钟,过期后由后续投递尝试重新取得处理权,不是后台定时器到点必然恢复。

重放前确认配置已修复,并检查交易、权益和外部副作用是否已经发生。已经成功的事件会被幂等逻辑跳过,重放它不会自动修复一条独立失败的异步 Command。

不要删除事件账本、修改状态为未处理,或伪造新事件 ID 强制重跑。若处理结果与外部状态不一致,应先完成对账,再通过对应业务层设计修复。

八、账单门户与退款

账单门户需要用户对应的 payment_customers 记录,Customer 必须属于当前连接与模式。再检查 Stripe 门户配置和返回地址,不能拿另一环境的 Customer 测试。

当前退款处理会保存退款记录,应用回调会记录 refund.updated,但不会自动撤销一次性购买授予的角色或权益。退款成功后仍能访问,不一定是回调失败,可能是尚未实现退款后的产品规则。订阅的终止流程应另外检查对应订阅事件。

修复后的验证

使用测试模式从应用创建一笔完整购买,确认交易、事件、权限和页面行为一致。再验证重复投递不重复发放,以及未购买用户仍不能访问受限功能。生产问题的恢复以原业务记录对账为准,不要求用户再次付款来证明修复有效。