支付与 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,但不会自动撤销一次性购买授予的角色或权益。退款成功后仍能访问,不一定是回调失败,可能是尚未实现退款后的产品规则。订阅的终止流程应另外检查对应订阅事件。
修复后的验证
使用测试模式从应用创建一笔完整购买,确认交易、事件、权限和页面行为一致。再验证重复投递不重复发放,以及未购买用户仍不能访问受限功能。生产问题的恢复以原业务记录对账为准,不要求用户再次付款来证明修复有效。